Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 to 3.2.0 and later. The source lives in
[`src/Bridge/Symfony/recipe/`](src/Bridge/Symfony/recipe).

#### Without Flex

Register the bundle in `config/bundles.php`:

Expand Down
102 changes: 102 additions & 0 deletions bin/build-flex-endpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

/**
* Builds a Symfony Flex recipe endpoint from src/Bridge/Symfony/recipe/.
*
* Flex reads recipes from an endpoint, never from the package itself: an
* `index.json` listing the recipe versions, plus one JSON file per version
* that carries the manifest and the files to copy. The release workflow
* attaches both to every GitHub release, so that applications can point
* `extra.symfony.endpoint` at `releases/latest/download/index.json`.
*
* The recipe source keeps the `symfony/recipes-contrib` layout
* (`<version>/manifest.json`, `<version>/config/...`), so it can be
* contributed there unchanged.
*
* Usage: php bin/build-flex-endpoint.php <output dir> <base url>
*
* 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 <output dir> <base url>\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<string, mixed> $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");
}
3 changes: 3 additions & 0 deletions src/Bridge/Symfony/recipe/3.2/config/packages/zammad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
zammad:
url: '%env(ZAMMAD_URL)%'
token: '%env(ZAMMAD_TOKEN)%'
15 changes: 15 additions & 0 deletions src/Bridge/Symfony/recipe/3.2/manifest.json
Original file line number Diff line number Diff line change
@@ -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 <fg=green>ZAMMAD_URL</> and <fg=green>ZAMMAD_TOKEN</> in <fg=green>.env.local</>"
]
}
17 changes: 16 additions & 1 deletion src/Bridge/SymfonyBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
*
Expand Down
52 changes: 28 additions & 24 deletions test/Install/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,36 +67,40 @@ 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

printf ' guzzlehttp/guzzle %s\n' "$(composer show guzzlehttp/guzzle | sed -n 's/^versions : \* //p')"
[ "$(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
Loading