symandy/makefile-maker-bundle
A Symfony bundle for generating and managing project Makefiles. It helps standardize common development commands, reduce repetitive terminal work, and keep build and maintenance tasks consistent across teams and environments.
This is a Symfony bundle, not a Laravel package — there is no service provider, no config/app.php entry, and it cannot be composer required into a Laravel app and "just work." The value for a Laravel developer is twofold: (1) if you maintain a Symfony service alongside Laravel apps, you can use it directly; (2) more commonly, you use it as a reference implementation for the same idea in Laravel (php artisan shortcuts in a Makefile).
Setup in a Symfony project:
composer require --dev symandy/makefile-maker-bundle
config/bundles.php if Flex doesn't do it (there is no official Flex recipe for a 1-star bundle, so expect to add it manually):
Symandy\MakefileMakerBundle\MakefileMakerBundle::class => ['dev' => true, 'test' => true],
bin/console list make
bin/console list | grep -i makefile
Look at src/Command/ in vendor/symandy/makefile-maker-bundle/ for the actual command name and its options (--output, --force, etc.).Makefile lands next to composer.json and bin/console.bin/console cache:clear, bin/console doctrine:migrations:migrate, bin/console messenger:consume — with make cc, make migrate, make worker.For a Laravel app, the equivalent starting point is a hand-written Makefile at the repo root wrapping php artisan (see Implementation Patterns). Do not try to install this bundle into Laravel; the Symfony Kernel, bundle registration, and config tree are hard requirements.
Treat the generated file as a scaffold, not a build artifact. Generate once, commit it, then own it. The generator's whole purpose is to save you the first 20 lines of typing; the long-term value is in the hand-tuned targets you add afterward.
Keep custom targets in a separate file so regeneration never eats your work:
# Makefile (generated)
include Makefile.local
# Makefile.local (yours, gitignored or committed — your call)
deploy: ## Deploy to production
bin/console cache:clear --env=prod
bin/console doctrine:migrations:migrate --no-interaction --env=prod
Standard shortcut vocabulary (adapt names to what the generator emits):
.PHONY: cc migrate assets test lint worker
cc: ## Clear cache
bin/console cache:clear
migrate: ## Run migrations
bin/console doctrine:migrations:migrate --no-interaction
assets: ## Build front-end assets
yarn install && yarn build
test: ## Run the test suite
bin/console --env=test doctrine:database:create --if-not-exists
bin/console --env=test doctrine:migrations:migrate --no-interaction
vendor/bin/phpunit
lint: ## Static analysis + code style
vendor/bin/phpstan analyse
vendor/bin/php-cs-fixer fix --dry-run --diff
Make it Docker-aware from day one. Hardcoding bin/console is the most common reason a Makefile stops being useful the moment the project containerizes:
EXEC ?= docker compose exec -T php
CONSOLE ?= $(EXEC) bin/console
migrate:
$(CONSOLE) doctrine:migrations:migrate --no-interaction
Now make migrate works locally and make migrate EXEC= runs on the host / in CI without editing the file.
Chain targets instead of writing shell one-liners, so the composed steps stay individually runnable and show up in make help:
deploy: assets migrate cc
@echo "deployed"
Self-documenting targets are worth the two extra lines, since a generated Makefile with 30 targets is otherwise a wall of names:
help: ## List available targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
.DEFAULT_GOAL := help
CI integration is the payoff: GitHub Actions / GitLab CI steps become make test, make lint, make assets, so the CI YAML and the local dev loop share one definition of "how to run the tests." This is the single strongest argument for the package's existence.
The Laravel translation of this pattern:
CONSOLE ?= php artisan
serve: ; $(CONSOLE) serve
fresh: ; $(CONSOLE) migrate:fresh --seed
routes: ; $(CONSOLE) route:list
tinker: ; $(CONSOLE) tinker
test: ; $(CONSOLE) test
queue: ; $(CONSOLE) queue:work --tries=3
Compare against the alternatives before committing: Composer scripts (portable, no make install needed, but no .PHONY/dependency graph and awkward on Windows), artisan custom commands (best when logic is non-trivial or needs to be shared with other developers who won't read the Makefile), and npm scripts (fine for front-end only). Make wins when you need composition across tools — artisan, phpunit, npm, docker, rsync all in one deploy target.
composer.json and be prepared to vendor/fork it. Read the source before relying on it — at this size the whole bundle is probably a couple of hundred lines.--force. If it's unconditional, keep the generated file and your edits in separate files via include..editorconfig (indent_style = space) rewrites the file. Add a .editorconfig override for Makefile and Makefile.local. Symptom: Makefile:12: *** missing separator. Stop..PHONY matters. Any target whose name matches a file or directory (test/, assets/, docs/) silently does nothing when that path exists. The generated file may or may not declare .PHONY — verify, and declare it yourself for every non-file target..DEFAULT_GOAL. Running bare make executes the first target in the file, which in a generated file is likely a cache-clear or a random command. Set .DEFAULT_GOAL := help explicitly.$(VAR) vs ${VAR}. Make expands $(...) first; shell variables need $$ to survive. The classic failure: make deploy VERSION=1.2.3 silently passing an empty string because a recipe used $VERSION instead of $(VERSION)/$$VERSION.bin/console defaults to dev; every CI or deploy-relevant target should pass --env=prod or --env=test explicitly rather than assuming APP_ENV is exported into the make process.make is not installed by default. Document WSL / Git Bash / choco install make in the README, or accept that Windows devs will run php bin/console ... directly.AsCommand classes, won't appear until you regenerate — which loops back to the overwrite risk.make -n <target> prints the commands without executing (catches quoting and variable expansion bugs), make -d for dependency resolution, and make <target> SHELL=/bin/bash -x when you need to see the actual shell lines.config/packages/makefile_maker.yaml (alias likely derived from the bundle name — confirm in src/DependencyInjection/Configuration.php). With no Flex recipe, any required config file must be created by hand; a missing node typically surfaces as a InvalidConfigurationException at container compile time.How can I help you explore Laravel packages today?