williarin/cook
Cook is a Laravel package for managing recipes: define ingredients, steps, and cooking details as Eloquent models, then query and display them in your app. It ships with migrations and models to get a recipe section running fast.
Cook is a Composer plugin, so it must be allowed before Composer will execute it. Install it in the project that should run the recipes (the "consumer"), not in the package that ships them:
composer require --dev williarin/cook
# Composer 2.2+ will prompt, or pre-allow it:
composer config allow-plugins.williarin/cook true
Then install any package that embeds a recipe. On composer install/update, the plugin reads the package's recipe definition (declared under extra in that package's composer.json, pointing at a recipe file shipped inside the package, typically a YAML file in a recipes/ or .cook/ directory) and executes the tasks for the matching install/update event.
Where to look first, in order:
composer.json → extra block, to see if the project itself declares recipe configuration or overrides.composer.json → extra → the recipe path it advertises.copy, append, replace, run, mkdir, remove, chmod, etc.).vendor/williarin/cook/ for the task handlers and the exact schema if a task's options are unclear.First use case: consume a recipe, then author one. Pick a package that ships a recipe and run a clean install with -vvv so you can see each task logged, then copy that recipe as the template for your own package. This is the fastest way to learn the schema because you get a working example that is guaranteed to match the installed plugin version.
Recipe ownership: package-side, not project-side. Recipes live in the package that needs to touch the host application — copying assets, appending config, seeding a directory, running a post-install command. The package author owns the file so it versions with the code that depends on it. Consumers should generally not edit it; they configure around it.
Install vs update vs uninstall. The main design decision in a recipe is which event each task belongs to. Treat install as "bootstrap from nothing" and update as "migrate from a previous version." Keep them separate even when they share tasks — an update block that re-copies a config file will silently overwrite user customisations on every bump, which is rarely what you want. Uninstall tasks (removing generated files, cleaning config) are worth writing for packages that scatter artifacts into the project.
Idempotency as a rule. Recipes re-run on update, so every task must be safe to execute twice. Prefer append with a guard comment, or tasks that overwrite a file the package fully owns, over blind append into a user-editable file. If a task cannot be made idempotent, put it only in the install block and accept that it runs once per project.
Copy vs template. For files the package fully owns (a stub, a scaffold, a binary), copy is fine. For files the user will edit after installation (config, .env fragments), copy once on install and never touch on update — otherwise you destroy their changes. A common pattern is copy-if-missing expressed as an install-only copy task plus no update counterpart.
Testing locally before publishing. Develop the recipe against a path repository so you get a real install cycle without pushing:
"repositories": [{ "type": "path", "url": "../my-package" }]
Then in a throwaway project run composer require my/package:@dev. Note that path repositories and version bumps interact awkwardly with the plugin's event detection — bump the version or remove vendor/my/package between runs to force the install event to fire again.
Ordering and composition. Recipes execute per-package as each package is installed, so ordering follows dependency resolution, not your intent. If package A's recipe must run before package B's, express it as a Composer dependency (B requires A) rather than relying on install order. Never assume a sibling package has already written its files.
CI and Docker. Recipes run at composer install time, so a Dockerfile that runs composer install --no-dev will execute them inside the image — which is usually right for scaffolding but wrong for anything that expects a mutable project tree at runtime. Pin --no-plugins for builds where you want the vendor tree but not the side effects, and run the full install in a separate step when you do want them.
Coexistence with Symfony Flex. Both plugins react to install/update events and both can write files. They do not coordinate. If a package ships both a Flex recipe and a Cook recipe, disable one path explicitly, or you will get duplicated appends (a classic symptom: the same block appears twice in config/bundles.php or a YAML file).
allow-plugins is the number-one failure mode. Composer 2.2+ blocks plugins by default. If nothing happens on install, check composer config allow-plugins — no error is raised for a blocked plugin, it is simply skipped, which looks identical to a recipe that "didn't work."
Recipes fire on package install/update, not on composer update of an unchanged package. If you edit a recipe inside vendor/ to test, nothing will run. Remove the package (composer remove, or delete vendor/<pkg> and the lock entry), bump the version, or use a path repo with @dev and re-require. Chasing a "broken recipe" that simply never re-triggered wastes a lot of time.
Paths are relative to the project root, not the package. This is the most common authoring bug: copy: { from: assets/foo, to: config/foo } resolves against the consuming project's working directory for the destination, while the source is resolved from the package. Verify both halves on a fresh install into an empty directory. Recipes that assume the package's own directory structure for dest will scatter files into vendor/ or fail silently.
Silent failure by design. Recipe tasks generally fail soft when a target directory does not exist or a pattern matches nothing. A recipe that "succeeded" may have done nothing. When debugging, run with -vvv and confirm each task is logged with the paths it resolved; do not trust exit code alone.
Write operations inside vendor/. Anything a recipe writes into vendor/ is lost on the next composer install (with --no-dev, or after a rm -rf vendor). Recipes should only write into the project tree — config dirs, public/, app source — never into the package's own vendor directory as a persistence strategy.
Permissions and ownership in containers/CI. run tasks execute as the user running Composer. In Docker builds that is often root, producing root-owned files in the project tree that break later local runs. Chmod/chown explicitly, or run Composer as the target UID.
run tasks are shell commands and inherit the environment. They see COMPOSER_* variables and run from the project root. They do not run in a login shell, so aliases and .bashrc additions are absent. Use absolute interpreter paths or rely on Composer's own binary resolution (e.g. vendor/bin/… relative to root) rather than assuming $PATH entries.
Extension points. Custom task types are where to look when the built-in set doesn't fit: the package's task registry is the natural place to hook in, and it's cleaner than abusing run with a one-liner script. Before that, check whether run plus a small PHP script in the package is sufficient — it usually is, and it keeps the recipe declarative.
Review recipes like code before composer install in production. A recipe is arbitrary post-install execution shipped by a dependency. If your process forbids Composer plugins (many security policies do), allow-plugins: false in the root composer.json disables it cleanly — the recipes simply won't run, and you take on the manual setup steps as documented by the package. Decide this policy once, at the project level, rather than per-package.
How can I help you explore Laravel packages today?