Tests, code style, refactoring, static analysis and CI for any PHP app — configured once in a single file, projected onto every tool that needs it.
➜ composer require leafs/alchemy --dev
➜ composer run test
Setting up tests with pest...
✓ 42 passed (0.31s)
➜ composer run ci
✓ Pipelines written from alchemy.yml
➜ vendor/bin/alchemy init
Detected Laravel project
✓ Imported phpunit.xml — suites, env, coverage
✓ Imported .php-cs-fixer.dist.php — rules, preset
✓ alchemy.yml created
➜ vendor/bin/alchemy switch gitlab --clean
✓ .gitlab-ci.yml written from the same alchemy.yml
✓ Removed .github/workflows
Works with PHP everywhere
alchemy init detects
your framework from composer.json and configures sensible defaults for
it — no framework required.
Intro
Alchemy is the QA layer for PHP. Instead of babysitting phpunit.xml, php-cs-fixer, rector and phpstan configs — and hand-writing the CI that runs them — you describe your policy once in alchemy.yml. Alchemy installs the tools, generates their config, runs them with honest exit codes, and writes your pipelines. It works with Leaf, Laravel, Symfony, Slim or no framework at all. And your AI assistant only needs to understand one QA file — not five unrelated formats.
One file
Everything below is generated, run, and cleaned up for you — your repo keeps exactly one QA file.
alchemy.yml
app:
- src
tests:
engine: pest
env:
APP_ENV: testing
lint:
preset: PSR12
refactor:
php: '8.3'
sets: [dead-code, code-quality]
analyse:
level: 6
actions:
provider: [github, gitlab]
run: [lint, tests, analyse]
What it replaces
phpunit.xml
suites, env vars, coverage — near 1:1, minus the XML
.php-cs-fixer.dist.php
every fixer rule works as-is
rector.php
prepared sets, PHP upgrade targets, skips
phpstan.neon
level, paths, ignores
.github/workflows/*.yml + .gitlab-ci.yml + .circleci/
matrices, caching, check-mode jobs — generated
…and the clutter they drag in
All of it swept into one gitignored .alchemy/ folder — your repo root stays exactly one QA file big.
Workflow
composer run test
Pest or PHPUnit, installed on first run. Parallel out of the box. Sample tests scaffolded on empty projects.
composer run lint · fmt
lint checks and fails CI without touching a file. fmt rewrites. You always know which one you're running.
composer run refactor · analyse
Rector refactors with prepared sets, PHPStan at your chosen level. Opt-in, gated in CI, honest exit codes.
Pest 5 ready · any stack
Pest 5 ships Test Impact Analysis, time-balanced sharding, a first-party PHPStan plugin and Rector rules — and still reads its config from a phpunit.xml, next to your neon file, your rector.php and your hand-written workflows. Alchemy folds all of it into the one file you already have.
alchemy.yml
tests:
engine: pest
flags: [tia] # only re-run affected tests
analyse:
level: 9
refactor:
sets: [phpunit-code-quality]
➜ composer run analyse
Your analyse paths cover your tests — adding pest's phpstan plugin...
✓ pest syntax analyses clean — it(), expect(), $this
Day-one Pest 5 support
flags: [tia] turns on Test Impact Analysis on every run. shard=1/4 splits CI by real timing. Pest's PHPStan plugin is installed and wired automatically when your analysis covers your tests — no neon editing, no extension-installer required.
Not a Leaf thing — a PHP thing
alchemy init detects Laravel, Symfony, Slim, Leaf or plain composer projects, imports the configs you already have — phpunit.xml, php-cs-fixer, phpstan neon, rector.php — or pins them untouched. Your call, recorded in the yml.
The file pile, retired
phpunit.xml, .php-cs-fixer.dist.php, phpstan.neon, phpstan-baseline.neon, rector.php, four workflow files — one alchemy.yml replaces the lot, generates configs out of sight in .alchemy/, and alchemy eject hands them all back if you ever leave.
The matrix
Alchemy speaks every engine's config language so you don't have to. Swap any of them without rewriting a thing.
Test engines
Pest · PHPUnit · paratest
Quality tools
CS Fixer · Rector · PHPStan
CI providers
GitHub · GitLab · CircleCI
Frameworks
Leaf · Laravel · Symfony · Slim
1 file
for your whole QA setup — instead of five configs in five formats.
0 deps
added to your project until the first command that needs an engine runs.
Pricing
Open source
$0forever
Alchemy Cloud Coming soon
The chain after the commit
FAQ
No. When
alchemy init
finds an existing config — phpunit.xml, php-cs-fixer, a phpstan
neon, a rector.php — it asks: port it into alchemy.yml, or keep
it. Keeping it is recorded as
tests: phpunit.xml
— a pinned file alchemy runs as-is, forever. Porting maps your
config into the yml so you can delete the original. Either way the
choice lives in alchemy.yml, not in alchemy's head.
No — requiring alchemy adds nothing else to your dependency tree.
Each engine installs the first time its command runs: pest on your
first
test,
rector on your first
refactor, and so on.
No. Alchemy is framework-agnostic —
alchemy init
detects Leaf, Laravel, Symfony, Slim or plain PHP from your
composer.json and configures sensible defaults for each.
Yes — that's the point. Generated pipelines run every tool in
check mode with real exit codes: style violations, pending
refactors and analysis findings all fail the build. Locally,
fmt
and
refactor
apply the fixes.
alchemy eject
exports a standard phpunit.xml and .php-cs-fixer.dist.php, rewires
your composer scripts to call the engines directly, and steps out
of the way. Your tests were always plain Pest/PHPUnit tests.
"Somewhere along the way we accepted that every new tool deserved another config file. I never thought that was progress."
Michael Darko · creator of Leaf PHP
Your pipeline, brewed before the coffee is.