The QA pipeline that brews itself

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

Leaf Laravel Symfony Slim Plain PHP Pest PHPUnit PHP CS Fixer Rector PHPStan GitHub Actions GitLab CI CircleCI Leaf Laravel Symfony Slim Plain PHP Pest PHPUnit PHP CS Fixer Rector PHPStan GitHub Actions GitLab CI CircleCI

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

One config to run your entire pipeline

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

  • .phpunit.result.cache
  • .php-cs-fixer.cache
  • .phpunit.cache/
  • rector cache · phpstan tmp
  • test-results, run logs, coverage output

All of it swept into one gitignored .alchemy/ folder — your repo root stays exactly one QA file big.

Workflow

One command for every question your code answers

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 just landed. Alchemy is the fastest way to actually run it.

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

Connect the tools behind your quality bar

Alchemy speaks every engine's config language so you don't have to. Swap any of them without rewriting a thing.

Pest
PHPUnit
CS Fixer
Rector
PHPStan
GitHub
GitLab
CircleCI

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

Free where it counts

Open source

$0forever

  • Tests, lint/fmt, refactor, analyse
  • CI generation for GitHub, GitLab, CircleCI
  • Framework detection + config import
  • Provider & engine switching
  • Eject anytime — no lock-in

Alchemy Cloud Coming soon

The chain after the commit

  • Pipeline insights across your repos
  • Debugging that starts where CI stops
  • Everything in that chain — done for you

FAQ

Answers to the questions that come up most

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

Get started today

Your pipeline, brewed before the coffee is.

Read the docs
$ composer require leafs/alchemy Copied to clipboard