From 88f5c19cac57b51cbe3f996c10c1676f589192f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 20 Sep 2023 16:53:05 +0200 Subject: [PATCH] feat: switch to FrankenPHP (#460) --- Dockerfile | 68 +++++++------------ README.md | 14 ++-- docker-compose.override.yml | 21 ++---- docker-compose.prod.yml | 9 +-- docker-compose.yml | 29 +++----- docker/php/conf.d/app.prod.ini | 2 - docker/php/docker-healthcheck.sh | 8 --- docker/php/php-fpm.d/zz-docker.conf | 9 --- docs/production.md | 13 ---- docs/tls.md | 23 ++++--- docs/xdebug.md | 2 +- {docker/caddy => frankenphp}/Caddyfile | 27 +++++++- {docker/php => frankenphp}/conf.d/app.dev.ini | 0 {docker/php => frankenphp}/conf.d/app.ini | 1 + frankenphp/conf.d/app.prod.ini | 2 + .../php => frankenphp}/docker-entrypoint.sh | 9 +-- frankenphp/worker.Caddyfile | 4 ++ 17 files changed, 93 insertions(+), 148 deletions(-) delete mode 100644 docker/php/conf.d/app.prod.ini delete mode 100644 docker/php/docker-healthcheck.sh delete mode 100644 docker/php/php-fpm.d/zz-docker.conf rename {docker/caddy => frankenphp}/Caddyfile (65%) rename {docker/php => frankenphp}/conf.d/app.dev.ini (100%) rename {docker/php => frankenphp}/conf.d/app.ini (93%) create mode 100644 frankenphp/conf.d/app.prod.ini rename {docker/php => frankenphp}/docker-entrypoint.sh (89%) create mode 100644 frankenphp/worker.Caddyfile diff --git a/Dockerfile b/Dockerfile index d2f2756..eb7b4b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,8 @@ #syntax=docker/dockerfile:1.4 # Versions -FROM php:8.2-fpm-alpine AS php_upstream -FROM mlocati/php-extension-installer:2 AS php_extension_installer_upstream +FROM dunglas/frankenphp:latest-alpine AS frankenphp_upstream FROM composer/composer:2-bin AS composer_upstream -FROM caddy:2-alpine AS caddy_upstream # The different stages of this Dockerfile are meant to be built into separate images @@ -12,10 +10,10 @@ FROM caddy:2-alpine AS caddy_upstream # https://docs.docker.com/compose/compose-file/#target -# Base PHP image -FROM php_upstream AS php_base +# Base FrankenPHP image +FROM frankenphp_upstream AS frankenphp_base -WORKDIR /srv/app +WORKDIR /app # persistent / runtime deps # hadolint ignore=DL3018 @@ -27,9 +25,6 @@ RUN apk add --no-cache \ git \ ; -# php extensions installer: https://github.com/mlocati/docker-php-extension-installer -COPY --from=php_extension_installer_upstream --link /usr/bin/install-php-extensions /usr/local/bin/ - RUN set -eux; \ install-php-extensions \ apcu \ @@ -41,17 +36,12 @@ RUN set -eux; \ ###> recipes ### ###< recipes ### -COPY --link docker/php/conf.d/app.ini $PHP_INI_DIR/conf.d/ +COPY --link frankenphp/conf.d/app.ini $PHP_INI_DIR/conf.d/ +COPY --link frankenphp/conf.d/app.ini $PHP_INI_DIR/conf.d/ +COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint +COPY --link frankenphp/Caddyfile /etc/caddy/Caddyfile -COPY --link docker/php/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf -RUN mkdir -p /var/run/php - -COPY --link --chmod=755 docker/php/docker-healthcheck.sh /usr/local/bin/docker-healthcheck -HEALTHCHECK --start-period=1m CMD docker-healthcheck - -COPY --link --chmod=755 docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint ENTRYPOINT ["docker-entrypoint"] -CMD ["php-fpm"] # https://getcomposer.org/doc/03-cli.md#composer-allow-superuser ENV COMPOSER_ALLOW_SUPERUSER=1 @@ -59,12 +49,14 @@ ENV PATH="${PATH}:/root/.composer/vendor/bin" COPY --from=composer_upstream --link /composer /usr/bin/composer +HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:2019/metrics || exit 1 +CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile" ] -# Dev PHP image -FROM php_base AS php_dev +# Dev FrankenPHP image +FROM frankenphp_base AS frankenphp_dev ENV APP_ENV=dev XDEBUG_MODE=off -VOLUME /srv/app/var/ +VOLUME /app/var/ RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" @@ -73,15 +65,20 @@ RUN set -eux; \ xdebug \ ; -COPY --link docker/php/conf.d/app.dev.ini $PHP_INI_DIR/conf.d/ +COPY --link frankenphp/conf.d/app.dev.ini $PHP_INI_DIR/conf.d/ -# Prod PHP image -FROM php_base AS php_prod +CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ] + +# Prod FrankenPHP image +FROM frankenphp_base AS frankenphp_prod ENV APP_ENV=prod +ENV FRANKENPHP_CONFIG="import worker.Caddyfile" RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" -COPY --link docker/php/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/ + +COPY --link frankenphp/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/ +COPY --link frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile # prevent the reinstallation of vendors at every changes in the source code COPY --link composer.* symfony.* ./ @@ -90,7 +87,7 @@ RUN set -eux; \ # copy sources COPY --link . ./ -RUN rm -Rf docker/ +RUN rm -Rf frankenphp/ RUN set -eux; \ mkdir -p var/cache var/log; \ @@ -98,22 +95,3 @@ RUN set -eux; \ composer dump-env prod; \ composer run-script --no-dev post-install-cmd; \ chmod +x bin/console; sync; - - -# Base Caddy image -FROM caddy_upstream AS caddy_base - -ARG TARGETARCH - -WORKDIR /srv/app - -# Download Caddy compiled with the Mercure and Vulcain modules -ADD --chmod=500 https://caddyserver.com/api/download?os=linux&arch=$TARGETARCH&p=github.com/dunglas/mercure/caddy&p=github.com/dunglas/vulcain/caddy /usr/bin/caddy - -COPY --link docker/caddy/Caddyfile /etc/caddy/Caddyfile -HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:2019/metrics || exit 1 - -# Prod Caddy image -FROM caddy_base AS caddy_prod - -COPY --from=php_prod --link /srv/app/public public/ diff --git a/README.md b/README.md index a11d803..d61496b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Symfony Docker -A [Docker](https://www.docker.com/)-based installer and runtime for the [Symfony](https://symfony.com) web framework, with full [HTTP/2](https://symfony.com/doc/current/weblink.html), HTTP/3 and HTTPS support. +A [Docker](https://www.docker.com/)-based installer and runtime for the [Symfony](https://symfony.com) web framework, +with [FrankenPHP](https://frankenphp.dev) and [Caddy](https://caddyserver.com/) inside! ![CI](https://github.com/dunglas/symfony-docker/workflows/CI/badge.svg) @@ -15,13 +16,14 @@ A [Docker](https://www.docker.com/)-based installer and runtime for the [Symfony ## Features * Production, development and CI ready +* Just 1 service by default +* Blazing-fast performance thanks to [the worker mode of FrankenPHP](https://github.com/dunglas/frankenphp/blob/main/docs/worker.md) (automatically enabled in prod mode) * [Installation of extra Docker Compose services](docs/extra-services.md) with Symfony Flex -* Automatic HTTPS (in dev and in prod!) -* HTTP/2, HTTP/3 and [Preload](https://symfony.com/doc/current/web_link.html) support -* Built-in [Mercure](https://symfony.com/doc/current/mercure.html) hub +* Automatic HTTPS (in dev and prod) +* HTTP/3 and [Early Hints](https://symfony.com/blog/new-in-symfony-6-3-early-hints) support +* Real-time messaging thanks to a built-in [Mercure hub](https://symfony.com/doc/current/mercure.html) * [Vulcain](https://vulcain.rocks) support * Native [XDebug](docs/xdebug.md) integration -* Just 2 services (PHP FPM and Caddy server) * Super-readable configuration **Enjoy!** @@ -43,4 +45,4 @@ Symfony Docker is available under the MIT License. ## Credits -Created by [Kévin Dunglas](https://dunglas.fr), co-maintained by [Maxime Helias](https://twitter.com/maxhelias) and sponsored by [Les-Tilleuls.coop](https://les-tilleuls.coop). +Created by [Kévin Dunglas](https://dunglas.dev), co-maintained by [Maxime Helias](https://twitter.com/maxhelias) and sponsored by [Les-Tilleuls.coop](https://les-tilleuls.coop). diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 8a45c59..c3c4574 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -5,30 +5,21 @@ services: php: build: context: . - target: php_dev + target: frankenphp_dev volumes: - - ./:/srv/app - - ./docker/php/conf.d/app.dev.ini:/usr/local/etc/php/conf.d/app.dev.ini:ro + - ./:/app + - ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro + - ./frankenphp/conf.d/app.dev.ini:/usr/local/etc/php/conf.d/app.dev.ini:ro # If you develop on Mac or Windows you can remove the vendor/ directory # from the bind-mount for better performance by enabling the next line: - #- /srv/app/vendor + #- /app/vendor environment: + MERCURE_EXTRA_DIRECTIVES: demo # See https://xdebug.org/docs/all_settings#mode XDEBUG_MODE: "${XDEBUG_MODE:-off}" extra_hosts: # Ensure that host.docker.internal is correctly defined on Linux - host.docker.internal:host-gateway - caddy: - command: [ "caddy", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ] - build: - context: . - target: caddy_base - volumes: - - ./public:/srv/app/public:ro - - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - environment: - MERCURE_EXTRA_DIRECTIVES: demo - ###> symfony/mercure-bundle ### ###< symfony/mercure-bundle ### diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 6795790..c8cfb06 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -5,15 +5,8 @@ services: php: build: context: . - target: php_prod + target: frankenphp_prod environment: APP_SECRET: ${APP_SECRET} - MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET} - - caddy: - build: - context: . - target: caddy_prod - environment: MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} diff --git a/docker-compose.yml b/docker-compose.yml index 5333e31..3d22dff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,34 +4,22 @@ services: php: image: ${IMAGES_PREFIX:-}app-php restart: unless-stopped - volumes: - - php_socket:/var/run/php environment: + SERVER_NAME: ${SERVER_NAME:-localhost}, php:80 + MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} + MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16} - TRUSTED_HOSTS: ^${SERVER_NAME:-example\.com|localhost}|caddy$$ - # The two next lines can be removed after initial installation - SYMFONY_VERSION: ${SYMFONY_VERSION:-} - STABILITY: ${STABILITY:-stable} + TRUSTED_HOSTS: ^${SERVER_NAME:-example\.com|localhost}|php$$ # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8} # Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration - MERCURE_URL: ${CADDY_MERCURE_URL:-http://caddy/.well-known/mercure} + MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure} MERCURE_PUBLIC_URL: https://${SERVER_NAME:-localhost}/.well-known/mercure MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} - - caddy: - image: ${IMAGES_PREFIX:-}app-caddy - depends_on: - php: - condition: service_healthy - restart: true - environment: - SERVER_NAME: ${SERVER_NAME:-localhost}, caddy:80 - MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} - MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} - restart: unless-stopped + # The two next lines can be removed after initial installation + SYMFONY_VERSION: ${SYMFONY_VERSION:-} + STABILITY: ${STABILITY:-stable} volumes: - - php_socket:/var/run/php - caddy_data:/data - caddy_config:/config ports: @@ -53,7 +41,6 @@ services: ###< symfony/mercure-bundle ### volumes: - php_socket: caddy_data: caddy_config: ###> symfony/mercure-bundle ### diff --git a/docker/php/conf.d/app.prod.ini b/docker/php/conf.d/app.prod.ini deleted file mode 100644 index 993d481..0000000 --- a/docker/php/conf.d/app.prod.ini +++ /dev/null @@ -1,2 +0,0 @@ -opcache.preload_user = www-data -opcache.preload = /srv/app/config/preload.php diff --git a/docker/php/docker-healthcheck.sh b/docker/php/docker-healthcheck.sh deleted file mode 100644 index f322de5..0000000 --- a/docker/php/docker-healthcheck.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e - -if env -i REQUEST_METHOD=GET SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping cgi-fcgi -bind -connect /var/run/php/php-fpm.sock; then - exit 0 -fi - -exit 1 diff --git a/docker/php/php-fpm.d/zz-docker.conf b/docker/php/php-fpm.d/zz-docker.conf deleted file mode 100644 index c01769e..0000000 --- a/docker/php/php-fpm.d/zz-docker.conf +++ /dev/null @@ -1,9 +0,0 @@ -[global] -daemonize = no -process_control_timeout = 20 - -[www] -listen = /var/run/php/php-fpm.sock -listen.mode = 0666 -ping.path = /ping -access.suppress_path[] = /ping diff --git a/docs/production.md b/docs/production.md index 31f3ac5..02fbd11 100644 --- a/docs/production.md +++ b/docs/production.md @@ -85,16 +85,3 @@ docker compose -f docker-compose.yml -f docker-compose.prod.yml up --wait If you want to deploy your app on a cluster of machines, you can use [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/), which is compatible with the provided Compose files. To deploy on Kubernetes, take a look at [the Helm chart provided with API Platform](https://api-platform.com/docs/deployment/kubernetes/), which can be easily adapted for use with Symfony Docker. - -## Configuring a Load Balancer or a Reverse Proxy - -Since Caddy 2.5, XFF values of incoming requests will be ignored to prevent spoofing. -So if Caddy is not the first server being connected to by your clients (for example when a CDN is in front of Caddy), you may configure `trusted_proxies` with a list of IP ranges (CIDRs) from which incoming requests are trusted to have sent good values for these headers. -As a shortcut, `private_ranges` may be configured to trust all private IP ranges. - -```diff --php_fastcgi unix//var/run/php/php-fpm.sock -+php_fastcgi unix//var/run/php/php-fpm.sock { -+ trusted_proxies private_ranges -+} -``` diff --git a/docs/tls.md b/docs/tls.md index ba4d9ce..094239f 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -7,11 +7,11 @@ You must add the authority to the trust store of the host : ``` # Mac -$ docker cp $(docker compose ps -q caddy):/data/caddy/pki/authorities/local/root.crt /tmp/root.crt && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/root.crt +$ docker cp $(docker compose ps -q php):/data/caddy/pki/authorities/local/root.crt /tmp/root.crt && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/root.crt # Linux -$ docker cp $(docker compose ps -q caddy):/data/caddy/pki/authorities/local/root.crt /usr/local/share/ca-certificates/root.crt && sudo update-ca-certificates +$ docker cp $(docker compose ps -q php):/data/caddy/pki/authorities/local/root.crt /usr/local/share/ca-certificates/root.crt && sudo update-ca-certificates # Windows -$ docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt %TEMP%/root.crt && certutil -addstore -f "ROOT" %TEMP%/root.crt +$ docker compose cp php:/data/caddy/pki/authorities/local/root.crt %TEMP%/root.crt && certutil -addstore -f "ROOT" %TEMP%/root.crt ``` ## Using Custom TLS Certificates @@ -23,16 +23,17 @@ For instance, to use self-signed certificates created with [mkcert](https://gith 1. Locally install `mkcert` 2. Create the folder storing the certs: - `mkdir docker/caddy/certs -p` + `mkdir frankenphp/certs -p` 3. Generate the certificates for your local host (example: "server-name.localhost"): - `mkcert -cert-file docker/caddy/certs/tls.pem -key-file docker/caddy/certs/tls.key "server-name.localhost"` -4. Add these lines to the `./docker-compose.override.yml` file about `CADDY_EXTRA_CONFIG` environment and volume for the `caddy` service : + `mkcert -cert-file frankenphp/certs/tls.pem -key-file frankenphp/certs/tls.key "server-name.localhost"` +4. Add these lines to the `./docker-compose.override.yml` file about `CADDY_EXTRA_CONFIG` environment and volume for the `php` service : ```diff - caddy: - + environment: + php: + environment: + CADDY_EXTRA_CONFIG: "tls /etc/caddy/certs/tls.pem /etc/caddy/certs/tls.key" + # ... volumes: - + - ./docker/caddy/certs:/etc/caddy/certs:ro - - ./public:/srv/app/public:ro + + - ./frankenphp/certs:/etc/caddy/certs:ro + - ./public:/app/public:ro ``` -5. Restart your `caddy` container +5. Restart your `php` service diff --git a/docs/xdebug.md b/docs/xdebug.md index 6b7346d..7ae2ccb 100644 --- a/docs/xdebug.md +++ b/docs/xdebug.md @@ -29,7 +29,7 @@ First, [create a PHP debug remote server configuration](https://www.jetbrains.co * Port: `443` * Debugger: `Xdebug` * Check `Use path mappings` - * Absolute path on the server: `/srv/app` + * Absolute path on the server: `/app` You can now use the debugger! diff --git a/docker/caddy/Caddyfile b/frankenphp/Caddyfile similarity index 65% rename from docker/caddy/Caddyfile rename to frankenphp/Caddyfile index 59690f6..5f48385 100644 --- a/docker/caddy/Caddyfile +++ b/frankenphp/Caddyfile @@ -1,5 +1,9 @@ { {$CADDY_GLOBAL_OPTIONS} + + frankenphp { + {$FRANKENPHP_CONFIG} + } } {$SERVER_NAME:localhost} @@ -19,7 +23,7 @@ log { } route { - root * /srv/app/public + root * /app/public mercure { # Transport to use (default to Bolt) transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db} @@ -36,7 +40,26 @@ route { } vulcain - php_fastcgi unix//var/run/php/php-fpm.sock + # Add trailing slash for directory requests + @canonicalPath { + file {path}/index.php + not path */ + } + redir @canonicalPath {path}/ 308 + + # If the requested file does not exist, try index files + @indexFiles file { + try_files {path} {path}/index.php index.php + split_path .php + } + rewrite @indexFiles {http.matchers.file.relative} + + # FrankenPHP! + @phpFiles path *.php + php @phpFiles + encode zstd gzip file_server + + respond 404 } diff --git a/docker/php/conf.d/app.dev.ini b/frankenphp/conf.d/app.dev.ini similarity index 100% rename from docker/php/conf.d/app.dev.ini rename to frankenphp/conf.d/app.dev.ini diff --git a/docker/php/conf.d/app.ini b/frankenphp/conf.d/app.ini similarity index 93% rename from docker/php/conf.d/app.ini rename to frankenphp/conf.d/app.ini index 79a17dd..f533f2d 100644 --- a/docker/php/conf.d/app.ini +++ b/frankenphp/conf.d/app.ini @@ -1,3 +1,4 @@ +variables_order = EGPS expose_php = 0 date.timezone = UTC apc.enable_cli = 1 diff --git a/frankenphp/conf.d/app.prod.ini b/frankenphp/conf.d/app.prod.ini new file mode 100644 index 0000000..3bcaa71 --- /dev/null +++ b/frankenphp/conf.d/app.prod.ini @@ -0,0 +1,2 @@ +opcache.preload_user = root +opcache.preload = /app/config/preload.php diff --git a/docker/php/docker-entrypoint.sh b/frankenphp/docker-entrypoint.sh similarity index 89% rename from docker/php/docker-entrypoint.sh rename to frankenphp/docker-entrypoint.sh index b9352f7..aba856b 100755 --- a/docker/php/docker-entrypoint.sh +++ b/frankenphp/docker-entrypoint.sh @@ -1,12 +1,7 @@ #!/bin/sh set -e -# first arg is `-f` or `--some-option` -if [ "${1#-}" != "$1" ]; then - set -- php-fpm "$@" -fi - -if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then +if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then # Install the project the first time PHP is started # After the installation, the following block can be deleted if [ ! -f composer.json ]; then @@ -18,7 +13,7 @@ if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then cd - rm -Rf tmp/ - composer require "php:>=$PHP_VERSION" + composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony composer config --json extra.symfony.docker 'true' if grep -q ^DATABASE_URL= .env; then diff --git a/frankenphp/worker.Caddyfile b/frankenphp/worker.Caddyfile new file mode 100644 index 0000000..d384ae4 --- /dev/null +++ b/frankenphp/worker.Caddyfile @@ -0,0 +1,4 @@ +worker { + file ./public/index.php + env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime +}