From 02b88490e850c59cd140974ae9f7825dec0b2cb5 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 9 Nov 2026 13:11:50 +0100 Subject: [PATCH 1/6] Symfony: add a Flex recipe for the bundle (#182) --- .gitattributes | 5 +++++ .../recipe/3.3/config/packages/zammad.yaml | 3 +++ src/Bridge/Symfony/recipe/3.3/manifest.json | 15 +++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 src/Bridge/Symfony/recipe/3.3/config/packages/zammad.yaml create mode 100644 src/Bridge/Symfony/recipe/3.3/manifest.json diff --git a/.gitattributes b/.gitattributes index 0c165ef..944e1c3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,11 +8,16 @@ # The README links into examples/ and docs/. Those links keep working where the # README is actually read (GitHub, Packagist); inside an installed vendor/ they # dangle, which is the trade for not shipping them. +# +# The Symfony Flex recipe under src/Bridge/Symfony/recipe/ is ignored although +# it sits in src/: Flex never reads it from the package, it gets the release +# assets that bin/build-flex-endpoint.php builds from it. /.github export-ignore /bin export-ignore /docs export-ignore /examples export-ignore /test export-ignore +/src/Bridge/Symfony/recipe export-ignore /.gitattributes export-ignore /.gitignore export-ignore /CODE_OF_CONDUCT.md export-ignore diff --git a/src/Bridge/Symfony/recipe/3.3/config/packages/zammad.yaml b/src/Bridge/Symfony/recipe/3.3/config/packages/zammad.yaml new file mode 100644 index 0000000..5aadc49 --- /dev/null +++ b/src/Bridge/Symfony/recipe/3.3/config/packages/zammad.yaml @@ -0,0 +1,3 @@ +zammad: + url: '%env(ZAMMAD_URL)%' + token: '%env(ZAMMAD_TOKEN)%' diff --git a/src/Bridge/Symfony/recipe/3.3/manifest.json b/src/Bridge/Symfony/recipe/3.3/manifest.json new file mode 100644 index 0000000..383e206 --- /dev/null +++ b/src/Bridge/Symfony/recipe/3.3/manifest.json @@ -0,0 +1,15 @@ +{ + "bundles": { + "ZammadAPIClient\\Bridge\\SymfonyBundle": ["all"] + }, + "copy-from-recipe": { + "config/": "%CONFIG_DIR%/" + }, + "env": { + "ZAMMAD_URL": "https://zammad.example.com", + "ZAMMAD_TOKEN": "" + }, + "post-install-output": [ + " * Set ZAMMAD_URL and ZAMMAD_TOKEN in .env.local" + ] +} From 190f4613a310e07484540c736129fa38ec8b7777 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 9 Nov 2026 13:11:53 +0100 Subject: [PATCH 2/6] Build a Flex recipe endpoint for release assets (#182) --- bin/build-flex-endpoint.php | 102 ++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 bin/build-flex-endpoint.php diff --git a/bin/build-flex-endpoint.php b/bin/build-flex-endpoint.php new file mode 100644 index 0000000..5d699b0 --- /dev/null +++ b/bin/build-flex-endpoint.php @@ -0,0 +1,102 @@ +/manifest.json`, `/config/...`), so it can be + * contributed there unchanged. + * + * Usage: php bin/build-flex-endpoint.php + * + * The base URL is where the generated files will be served from, e.g. + * `https://github.com/zammad/zammad-api-client-php/releases/latest/download`. + */ + +const PACKAGE = 'zammad/zammad-api-client-php'; +const SOURCE = __DIR__ . '/../src/Bridge/Symfony/recipe'; + +[, $outDir, $baseUrl] = $argv + [null, null, null]; +if ($outDir === null || $baseUrl === null) { + fwrite(STDERR, "Usage: php bin/build-flex-endpoint.php \n"); + exit(2); +} + +if (!is_dir($outDir) && !mkdir($outDir, 0777, true)) { + fwrite(STDERR, "Cannot create $outDir\n"); + exit(1); +} + +$dotted = str_replace('/', '.', PACKAGE); +$versions = []; + +foreach (glob(SOURCE . '/*/manifest.json') ?: [] as $manifestFile) { + $dir = dirname($manifestFile); + $version = basename($dir); + + $files = []; + $entries = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)); + foreach ($entries as $entry) { + $relative = substr($entry->getPathname(), strlen($dir) + 1); + if ($relative === 'manifest.json') { + continue; + } + $files[$relative] = [ + 'contents' => explode("\n", (string) file_get_contents($entry->getPathname())), + 'executable' => false, + ]; + } + ksort($files); + + $recipe = [ + 'manifests' => [ + PACKAGE => [ + 'manifest' => json_decode((string) file_get_contents($manifestFile), true, 512, JSON_THROW_ON_ERROR), + 'files' => $files, + // Flex records this in symfony.lock to tell later recipe updates apart. + 'ref' => sha1(json_encode([$version, file_get_contents($manifestFile), $files], JSON_THROW_ON_ERROR)), + ], + ], + ]; + + write("$outDir/$dotted.$version.json", $recipe); + $versions[] = $version; +} + +if ($versions === []) { + fwrite(STDERR, 'No recipe found under ' . SOURCE . "\n"); + exit(1); +} +usort($versions, 'version_compare'); + +write("$outDir/index.json", [ + 'recipes' => [PACKAGE => $versions], + 'branch' => 'main', + // Not a contrib endpoint: the application opted in by adding it, so Flex + // must apply the recipe without asking for `allow-contrib`. With `true` + // a non-interactive install would skip it without a word. + 'is_contrib' => false, + '_links' => [ + 'repository' => 'github.com/zammad/zammad-api-client-php', + 'origin_template' => '{package}:{version}@github.com/zammad/zammad-api-client-php:releases', + 'recipe_template' => rtrim($baseUrl, '/') . '/{package_dotted}.{version}.json', + ], +]); + +echo implode("\n", array_map(static fn (string $v): string => "$outDir/$dotted.$v.json", $versions)), "\n$outDir/index.json\n"; + +/** + * @param array $data + */ +function write(string $file, array $data): void +{ + file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n"); +} From 30cdeeff888b34bece42c02761b624f08eb2980a Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 9 Nov 2026 13:11:55 +0100 Subject: [PATCH 3/6] CI: attach the Flex recipe endpoint to every release (#182) --- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5a5cdd5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release assets + +# Attaches the Symfony Flex recipe endpoint to every published release, so that +# applications can use releases/latest/download/index.json as a recipe source +# (#182). Run it by hand to attach the files to a release that lacks them. +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Release tag to attach the Flex endpoint to" + required: true + +permissions: + contents: write + +jobs: + flex-endpoint: + runs-on: ubuntu-latest + env: + TAG: ${{ github.event.release.tag_name || inputs.tag }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ env.TAG }} + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + # The recipe files must be served from `latest`, not from this tag: the + # index is fetched through releases/latest/download, and Flex resolves + # the recipe URL from the index it got. + - run: php bin/build-flex-endpoint.php build/flex "https://github.com/${{ github.repository }}/releases/latest/download" + - run: gh release upload "$TAG" build/flex/*.json --clobber + env: + GH_TOKEN: ${{ github.token }} From e24de830d74b5e525a9efb4a98291bccaf610ed9 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 9 Nov 2026 13:11:58 +0100 Subject: [PATCH 4/6] Test: apply the Flex recipe in the Symfony install test (#182) --- test/Install/run.sh | 52 ++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/test/Install/run.sh b/test/Install/run.sh index 6c9ee17..d195210 100755 --- a/test/Install/run.sh +++ b/test/Install/run.sh @@ -67,32 +67,28 @@ laravel) ok 'provider found by package discovery' ;; symfony) + # Serves the Flex recipe the way the release assets do (#182), built by the + # same script from the same source. + endpoint=$(mktemp -d) + php "$root/bin/build-flex-endpoint.php" "$endpoint" http://127.0.0.1:8099 >/dev/null + php -S 127.0.0.1:8099 -t "$endpoint" >/dev/null 2>&1 & + trap "kill $!" EXIT + until php -r 'exit(@fsockopen("127.0.0.1", 8099) ? 0 : 1);'; do sleep 0.2; done + composer config secure-http false # the local endpoint is plain http + composer config --json extra.symfony.endpoint '["http://127.0.0.1:8099/index.json", "flex://defaults"]' + + before=$(cksum config/bundles.php .env) composer require --quiet 'zammad/zammad-api-client-php:^3.2' \ || fail 'composer require failed' - # Without a Flex recipe the bundle needs the two manual steps from the - # README. Once the recipe exists (#182) these branches are no longer taken. - if grep -q 'ZammadAPIClient\\Bridge\\SymfonyBundle::class' config/bundles.php; then - ok 'bundle registered by Flex' - else - php -r ' - $file = "config/bundles.php"; - $bundle = " ZammadAPIClient\\Bridge\\SymfonyBundle::class => [\"all\" => true],\n];"; - file_put_contents($file, preg_replace("/^\];/m", $bundle, file_get_contents($file))); - ' - ok 'bundle registered by hand (no Flex recipe yet, #182)' - fi - if [ -f config/packages/zammad.yaml ]; then - ok 'config/packages/zammad.yaml created by Flex' - else - echo 'zammad: ~' > config/packages/zammad.yaml - ok 'config/packages/zammad.yaml created by hand (no Flex recipe yet, #182)' - fi - # `composer require` has just warmed the container cache, and the edits - # above land within the same second. Symfony compares modification times - # at one-second resolution and keeps the stale container, which even - # `cache:clear` reuses for its warm-up. A user editing by hand never hits - # this, so the cache is simply dropped. - rm -rf var/cache + grep -q 'ZammadAPIClient\\Bridge\\SymfonyBundle::class' config/bundles.php \ + || fail 'bundle not registered by the recipe' + ok 'bundle registered by the recipe' + [ -f config/packages/zammad.yaml ] \ + || fail 'config/packages/zammad.yaml not created by the recipe' + ok 'config/packages/zammad.yaml created by the recipe' + grep -q '^ZAMMAD_URL=' .env && grep -q '^ZAMMAD_TOKEN=' .env \ + || fail 'ZAMMAD_URL / ZAMMAD_TOKEN not added to .env' + ok 'ZAMMAD_URL / ZAMMAD_TOKEN added to .env' ;; esac @@ -100,3 +96,11 @@ printf ' guzzlehttp/guzzle %s\n' "$(composer show guzzlehttp/guzzle | sed -n 's [ "$(resolve_$framework)" = 'ZammadAPIClient\ZammadClient' ] \ || fail 'ZammadClient does not resolve from the container' ok 'ZammadClient resolves from the container' + +if [ "$framework" = symfony ]; then + composer remove --quiet zammad/zammad-api-client-php \ + || fail 'composer remove failed' + [ "$(cksum config/bundles.php .env)" = "$before" ] && [ ! -e config/packages/zammad.yaml ] \ + || fail 'composer remove left recipe changes behind' + ok 'composer remove reverts the recipe' +fi From 24e14e8ff48932578f9ce6a31be603e0fcbf9c86 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 9 Nov 2026 13:12:00 +0100 Subject: [PATCH 5/6] Docs: describe the Flex setup for Symfony (#182) --- CHANGELOG.md | 3 +++ README.md | 28 +++++++++++++++++++++++++++- src/Bridge/SymfonyBundle.php | 17 ++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 981dc91..eca026a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Added +- Symfony Flex recipe, published as assets of every GitHub release. With `releases/latest/download/index.json` added to `extra.symfony.endpoint`, `composer require` registers the bundle, creates `config/packages/zammad.yaml` and adds `ZAMMAD_URL` / `ZAMMAD_TOKEN` to `.env`; `composer remove` reverts it. Applications without Flex keep registering the bundle by hand ([#182](https://github.com/zammad/zammad-api-client-php/issues/182)) + ### Changed - Guzzle 8 is supported: `guzzlehttp/guzzle` is now `^7.10 || ^8.0` and `guzzlehttp/psr7` is now `^2.7 || ^3.0`. Before, installing the client into a project already on Guzzle 8, such as every new Laravel 13 application, failed unless Composer was allowed to downgrade Guzzle ([#180](https://github.com/zammad/zammad-api-client-php/issues/180), [#181](https://github.com/zammad/zammad-api-client-php/issues/181)) diff --git a/README.md b/README.md index fb28aaa..25bf675 100644 --- a/README.md +++ b/README.md @@ -485,7 +485,33 @@ Only if you opt this package out of package discovery: register ### Symfony Supported: **Symfony 6.4 LTS, 7.x and 8.x**. Each of those is exercised in CI — -see the `bridge-symfony` job in [`.github/workflows/tests.yml`](.github/workflows/tests.yml). +see the `bridge-symfony` and `install` jobs in [`.github/workflows/tests.yml`](.github/workflows/tests.yml). + +#### With Symfony Flex + +Every release ships a Flex recipe as release assets. Add it as a recipe source +once, **before** requiring the package, then require it as usual: + +```bash +composer config --json extra.symfony.endpoint \ + '["https://github.com/zammad/zammad-api-client-php/releases/latest/download/index.json", "flex://defaults"]' +composer require zammad/zammad-api-client-php +``` + +The recipe registers the bundle, creates `config/packages/zammad.yaml` and adds +`ZAMMAD_URL` / `ZAMMAD_TOKEN` to `.env` — set the real values in `.env.local`. +`composer remove` takes all of it out again. + +- Keep `flex://defaults` in the list, it is what serves Symfony's own recipes. If + your `composer.json` already has an `extra.symfony.endpoint` list, add the URL + to it instead of replacing it. +- Already installed the package? `composer recipes:install zammad/zammad-api-client-php --force` + applies the recipe afterwards. `--force` overwrites an existing + `config/packages/zammad.yaml`, so back up a hand-written one first. +- The recipe applies from 3.3.0 on. The source lives in + [`src/Bridge/Symfony/recipe/`](src/Bridge/Symfony/recipe). + +#### Without Flex Register the bundle in `config/bundles.php`: diff --git a/src/Bridge/SymfonyBundle.php b/src/Bridge/SymfonyBundle.php index 86082b9..8d6e98b 100644 --- a/src/Bridge/SymfonyBundle.php +++ b/src/Bridge/SymfonyBundle.php @@ -21,7 +21,22 @@ * {@see ZammadClient} for autowiring, and exposes an `zammad_client` alias — so * any service can type-hint `ZammadClient` and get a ready-to-use instance. * - * **Setup** + * **Setup with Symfony Flex** + * + * Every release ships a Flex recipe as release assets. Add it as a recipe + * source once, before requiring the package: + * + * ```bash + * composer config --json extra.symfony.endpoint \ + * '["https://github.com/zammad/zammad-api-client-php/releases/latest/download/index.json", "flex://defaults"]' + * composer require zammad/zammad-api-client-php + * ``` + * + * The recipe registers the bundle, creates `config/packages/zammad.yaml` and + * adds `ZAMMAD_URL` / `ZAMMAD_TOKEN` to `.env` — the three steps below. The + * recipe source is `src/Bridge/Symfony/recipe/`. + * + * **Setup without Flex** * * - Register the bundle in `config/bundles.php`: * From 42fee896071c83c9fe626f98d66a8a1e007b6d01 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Mon, 9 Nov 2026 13:26:07 +0100 Subject: [PATCH 6/6] Symfony: let the Flex recipe apply from 3.2 on (#182) --- README.md | 2 +- .../Symfony/recipe/{3.3 => 3.2}/config/packages/zammad.yaml | 0 src/Bridge/Symfony/recipe/{3.3 => 3.2}/manifest.json | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/Bridge/Symfony/recipe/{3.3 => 3.2}/config/packages/zammad.yaml (100%) rename src/Bridge/Symfony/recipe/{3.3 => 3.2}/manifest.json (100%) diff --git a/README.md b/README.md index 25bf675..dd46077 100644 --- a/README.md +++ b/README.md @@ -508,7 +508,7 @@ The recipe registers the bundle, creates `config/packages/zammad.yaml` and adds - Already installed the package? `composer recipes:install zammad/zammad-api-client-php --force` applies the recipe afterwards. `--force` overwrites an existing `config/packages/zammad.yaml`, so back up a hand-written one first. -- The recipe applies from 3.3.0 on. The source lives in +- The recipe applies to 3.2.0 and later. The source lives in [`src/Bridge/Symfony/recipe/`](src/Bridge/Symfony/recipe). #### Without Flex diff --git a/src/Bridge/Symfony/recipe/3.3/config/packages/zammad.yaml b/src/Bridge/Symfony/recipe/3.2/config/packages/zammad.yaml similarity index 100% rename from src/Bridge/Symfony/recipe/3.3/config/packages/zammad.yaml rename to src/Bridge/Symfony/recipe/3.2/config/packages/zammad.yaml diff --git a/src/Bridge/Symfony/recipe/3.3/manifest.json b/src/Bridge/Symfony/recipe/3.2/manifest.json similarity index 100% rename from src/Bridge/Symfony/recipe/3.3/manifest.json rename to src/Bridge/Symfony/recipe/3.2/manifest.json