from composer require to your own private recipe
Symfony Flex Recipes automatically create configuration files, directories, and environment variables on composer require. Once you understand how manifest.json is structured and how the contrib repository works, you can build your own recipes for internal bundles and distribute them reproducibly across teams.
Table of Contents
- 1. What Symfony Flex Recipes actually are
- 2. The recipe lifecycle: from composer require to finished configuration
- 3. Anatomy of a recipe: manifest.json in detail
- 4. Official repository vs. contrib repository
- 5. Creating and hosting your own private recipes
- 6. Unpacking recipes and adjusting them precisely
- 7. Post-install output and copy-from-package
- 8. Recipes in CI/CD and reproducible builds
- 9. Recipes compared to manual configuration
- 10. Summary
- 11. FAQ
1. What Symfony Flex Recipes actually are
A Symfony Flex Recipe is a small, versioned set of instructions that describes precisely which files, directories, and configuration entries a Composer package should bring into a Symfony project when it is installed. Without Flex Recipes, every team would need to manually create the matching config/packages/mailer.yaml after composer require symfony/mailer, add the .env variables, and check whether a bundle needs to be registered in bundles.php. Flex Recipes automate exactly this step.
Technically, a recipe is not part of the actual Composer package but a separate entry in a recipe repository, linked to the package via its name and a version constraint. This separation matters: a package maintainer can update a recipe independently of the package release, and a team can decide whether it trusts a recipe without changing the package's code at all. The symfony/flex Composer plugin is the component that establishes this link at install time.
This article goes beyond the surface of Flex Recipes: how is a recipe structured internally, how does the official repository differ from the contrib repository, and above all, how do you build your own private recipe for an internal bundle that should be installed just as automatically as an official Symfony package.
2. The recipe lifecycle: from composer require to finished configuration
When composer require installs a package, the Flex plugin checks after Composer's own installation step whether a recipe exists for that package and version. To do this it queries the configured recipe endpoint, by default https://raw.githubusercontent.com/symfony/recipes/flex/main/index.json for official recipes and a separate repository for contrib recipes. If a matching recipe is found, Flex downloads its content and applies the operations defined in it.
These operations are declarative: files from the recipe are copied to defined target paths in the project, entries are appended to .env, and bundle classes are registered in config/bundles.php. After successful application, Flex writes an entry into symfony.lock recording the package name, recipe version, and applied files. This lock file is the key to reproducibility: a second composer install on another machine applies the exact same recipe versions, because symfony.lock is checked into version control.
{
"symfony/mailer": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.1",
"ref": "b09e162839da9b0027bee6b06024b9089dab9d75"
},
"files": [
"config/packages/mailer.yaml"
]
}
}
This excerpt from a symfony.lock shows exactly which recipe was applied for which package version, including its Git reference. If the package is updated later and a newer recipe version exists, composer recipes shows that an update is available, but does not apply it automatically, so as not to overwrite existing manual adjustments.
3. Anatomy of a recipe: manifest.json in detail
At the center of every recipe is manifest.json, which describes all the operations Flex should perform on installation. The most important keys are bundles for automatic registration in config/bundles.php, copy-from-recipe for files that are copied unchanged, and env for entries appended to the .env file. In addition there is gitignore for entries in .gitignore and post-install-output for a hint message shown in the terminal after installation.
{
"bundles": {
"App\\CustomLoggingBundle\\CustomLoggingBundle": ["all"]
},
"copy-from-recipe": {
"config/": "%CONFIG_DIR%/"
},
"env": {
"CUSTOM_LOGGING_DSN": "custom-log://localhost:9000"
},
"gitignore": [
"/var/custom-logs/"
],
"post-install-output": [
"The bundle has been installed and enabled in all environments.",
"Set CUSTOM_LOGGING_DSN in your .env.local file for local development."
]
}
The %CONFIG_DIR% placeholder in copy-from-recipe is replaced at install time with the project's actual configuration path, by default config. This indirection allows a recipe to work independently of individual project structures, as long as the default convention is followed. The bundles key with the value ["all"] registers the bundle for every environment; alternatively individual environments such as ["dev", "test"] can be specified if a bundle should only be active in certain contexts.
4. Official repository vs. contrib repository
Symfony maintains two separate recipe repositories: symfony/recipes for official packages maintained by the Symfony core team or closely related projects, and symfony/recipes-contrib for community recipes covering third-party packages. The key difference is the trust level: recipes from the contrib repository are, by default, only applied by Flex after explicit confirmation, whereas official recipes run automatically.
This confirmation prompt is not a mere formality but a deliberate security mechanism: a contrib recipe can write arbitrary files and set environment variables, and a team should be aware of these changes before they are applied automatically. The symfony.lock file, combined with the extra.symfony.allow-contrib entry in composer.json, allows this behavior to be controlled for CI environments where interactive confirmation is not possible.
{
"extra": {
"symfony": {
"allow-contrib": false,
"endpoint": [
"https://api.github.com/repos/my-org/private-recipes/contents/index.json",
"flex://defaults"
]
}
}
}
With allow-contrib: false a team disables contrib recipes globally and forces every third-party package installation to proceed without automatic configuration, which is often desired in heavily regulated environments. The additional endpoint entry already shows how a private recipe repository is added alongside the default endpoints, which is covered in more depth in the next section.
5. Creating and hosting your own private recipes
A dedicated recipe becomes worthwhile for internal bundles as soon as more than one project uses the same bundle and manual configuration is repeated after every composer require. A private recipe repository is structurally identical to the official one: a Git repository with an index.json that points to individual manifest.json files per package and version. This repository can live on GitHub, GitLab, or a self-hosted Git server, as long as it is reachable through one of the endpoint formats Flex supports.
The structure follows the pattern {vendor}/{package}/{major.minor}/manifest.json. For an internal bundle acme/audit-log-bundle at version 2.x the recipe lives under acme/audit-log-bundle/2.0/manifest.json, complemented by an entry in the central index.json that references the package name and supported version ranges.
{
"acme/audit-log-bundle": {
"versions": {
"2.0": {
"version": "2.0",
"ref": "main"
}
}
}
}
In the composer.json of every project that should use this private recipe, the custom endpoint is added alongside the default endpoints, as shown in the previous section. For teams using Private Packagist it makes sense to host the recipe repository right next to the private Composer packages, so access rights can be managed through the same authentication system instead of maintaining a separate permission model for the recipe repository.
6. Unpacking recipes and adjusting them precisely
Not every recipe fits an existing project one to one, especially when additional adjustments to the generated configuration are needed. The command composer symfony:recipes:install {package} --force -v reapplies a recipe even if it was already installed, which is useful when a file was accidentally deleted. To deliberately remove Flex's management of a package, so the generated configuration can be freely adjusted without Flex managing it further on future updates, composer symfony:recipes serves as an overview command, followed by manually removing the entry from symfony.lock.
# List all installed recipes and their current status
composer symfony:recipes
# Re-apply a specific recipe, useful after accidentally deleting generated files
composer symfony:recipes:install symfony/mailer --force -v
# Show the raw recipe manifest for a package before deciding to trust it
composer symfony:recipes:install symfony/mailer --dry-run -v
The --dry-run mode shows exactly which files a recipe would write and which environment variables it would set, without actually changing anything. This is a sensible intermediate step, especially for contrib recipes, before granting the interactive confirmation, because it lets you see in advance whether the recipe would make other, unexpected changes beyond the expected configuration file.
7. Post-install output and copy-from-package
Alongside copy-from-recipe, which copies files from the recipe itself, there is copy-from-package, which copies files directly from the installed Composer package into the project. This is useful when a configuration file is tightly coupled to a specific package version and shipped inside the package itself instead of being maintained separately in the recipe repository, which avoids redundancy when the package and recipe are maintained by the same team.
post-install-output is the most underestimated part of a recipe, because it is the only direct communication channel to the developer who just installed the package. A good recipe uses this hint to state exactly which manual steps remain, such as setting an API key in .env.local, instead of leaving the developer to search the documentation separately.
{
"copy-from-package": {
"config/audit-log.dist.yaml": "%CONFIG_DIR%/packages/audit_log.yaml"
},
"post-install-output": [
"AcmeAuditLogBundle has been installed.",
" * Set AUDIT_LOG_API_KEY in your .env.local file.",
" * Run 'bin/console acme:audit-log:init' once to create the storage table."
]
}
A good benchmark for the quality of your own recipe: can a new team member become productive right after composer require acme/audit-log-bundle using only the post-install-output messages, without consulting a wiki or README? If so, the recipe is complete enough to be used by default across the team.
8. Recipes in CI/CD and reproducible builds
Because symfony.lock records every applied recipe version along with its Git reference, composer install in the CI pipeline behaves deterministically: the same recipe version is applied as during the original composer require, regardless of whether the recipe repository has since evolved. This is a decisive difference from manual configuration, which can silently drift from documentation without any build noticing.
In CI environments without an interactive terminal, allow-contrib must be set explicitly, because otherwise Flex waits for a confirmation that never comes and the build hangs. The combination of COMPOSER_NO_INTERACTION=1 as an environment variable and an explicit allow-contrib setting in composer.json ensures that recipe application in the pipeline behaves predictably, without manual intervention and without accidentally auto-accepting contrib recipes that should actually require a deliberate decision.
9. Recipes compared to manual configuration
The table below compares Flex Recipes to the classic manual configuration approach common before Symfony Flex, or in projects that don't use Flex.
| Aspect | Manual configuration | Flex recipe | Benefit |
|---|---|---|---|
| Time to working setup | 5 to 15 minutes | Seconds | Automatic file creation |
| Consistency across projects | Depends on documentation being followed | Identical per recipe version | Less configuration drift |
| Reproducibility in CI | Not guaranteed | Fixed via symfony.lock | Deterministic builds |
| Transparency of changes | Fully traceable manually | Visible via --dry-run before applying | Both approaches auditable |
The only area where manual configuration retains a genuine advantage is full control without any automation, for example with very unusual project structures that deviate from the standard convention. For the vast majority of Symfony projects, the benefit of Flex Recipes clearly outweighs this, especially once several projects share the same internal packages.
Mironsoft
Symfony bundle development and internal Composer infrastructure
Want your own Symfony Flex Recipes for internal bundles?
We build private recipe repositories for your internal bundles, set up the recipe endpoint for all your projects, and make sure composer require works as smoothly for you as it does for official Symfony packages.
Recipe development
manifest.json for your bundles including configuration files
Private repository
Hosted alongside your Private Packagist with unified access control
CI integration
Reproducible builds with the correct allow-contrib configuration
10. Summary
Symfony Flex Recipes automate exactly the steps that would otherwise need to be repeated manually after every composer require: creating configuration files, setting environment variables, registering bundles. manifest.json is the core of every recipe, with clearly defined keys for file copies, environment variables, and hint messages. The official repository and the contrib repository differ mainly in the trust level, which is expressed through the interactive confirmation prompt.
For teams with multiple projects and internal bundles, a dedicated private recipe repository pays off, structured identically to the official one and wired in through an additional endpoint in each project's composer.json. The symfony.lock file makes recipe application reproducible and CI friendly, while --dry-run before applying a third-party recipe provides full transparency about the planned changes. Combining these building blocks turns Flex Recipes into a genuine part of your own Symfony infrastructure, rather than a convenience limited to official packages.
Symfony Flex Recipes — The Essentials at a Glance
manifest.json
Defines bundles, copy-from-recipe, env, gitignore and post-install-output for automated installation.
Official vs. contrib
Contrib recipes need interactive confirmation, allow-contrib controls this behavior for CI environments.
Custom recipes
Private Git repository with index.json, plus an additional endpoint entry in every project's composer.json.
symfony.lock
Pins recipe version and Git reference, making composer install fully reproducible in CI.