# Booster: everything in one file > Booster plans and runs PHP and Laravel upgrades one hop at a time on Rector, gates every hop with your tests on the target PHP, and never commits or pushes for you. The README and the Markdown twin of every page, joined. The index is https://booster.apps.wayne.edu/llms.txt. Written by Squire's agent-ready kit; do not edit. --- ## README Source: https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/README.md ## Booster **Boosts your versions.** One dose at a time, and every dose gets a stamp. **Not to be confused with:** Booster moves an app to a newer PHP or framework version; Pinstripe's Rector set only modernizes code within the PHP version the app already runs. Checks: `composer ci` (the same command runs on GitLab and GitHub). Documentation: the docs live at [booster.apps.wayne.edu/docs/](https://booster.apps.wayne.edu/docs/) and are built from `docs/` by `bin/docs-site.php`. ### Where the code lives The canonical repository is on nougat: [enterprise-apps/booster](https://nougat.snu.gs/enterprise-apps/booster). git.wayne.edu and GitHub ([waynestate/booster](https://github.com/waynestate/booster)) are read-only mirrors of it. Open issues and merge requests on nougat, not on the mirrors. This moves to git.wayne.edu once it has CI runners. Nougat is the shot; the mirrors are the bandage. > A booster shot keeps your immunity current. This one keeps your PHP current, and it stings less. Front page: [`site/index.html`](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/site/index.html), a static page you can open in a browser or serve from `site/`. On Citadel it is served by the raw-PHP deploy layer in [`deploy/`](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/deploy/): a Squire kit whose front controller serves `site/` behind HELMET's security headers. For local development, keep serving `site/` directly. Docs: the same site serves the documentation at [`/docs/`](https://booster.apps.wayne.edu/docs/). It is built from [`docs/`](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/docs/) by `bin/docs-site.php` (`composer docs`), the built pages are committed, and `composer docs:check` (part of `composer ci`) fails when they drift from the sources. The docs wear the front page's look through the builder's `theme` key in `docs/docs.json`: [`docs/_header.html`](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/docs/_header.html) is the masthead every docs page gets (keep its links in step with the front page's), and [`docs/theme/booster.css`](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/docs/theme/booster.css) holds the palette, type, code blocks and the derived dark mode; the build copies it to `site/docs/theme/`. Edit those two files, never `site/assets/docs.css` or `docs.js` (Squire's kit), then run `composer docs`. Like a booster shot, it only works if you actually take it. Booster is WELD's upgrade tool. It plans the hops that take a WSU PHP app from the PHP or Laravel version it runs now to one the version policy still supports, and it runs them one at a time: [Rector](https://getrector.com) rewrites the code, Booster raises the constraints in `composer.json`, and the app's own tests must pass on the target PHP before the next hop starts. It stands on `rector/rector` and `driftingly/rector-laravel` and adds only the ordering, the review threshold, the gate, the advisory scans and a few WSU recipes. Booster edits your working copy and prints a report. It never commits to, pushes or opens a merge request on your app (the only merge requests it opens are its own hop-data refresh on Booster itself): a person reads the diff and merges it. ### Where it fits in WELD WELD is the Wayne State application stack. Booster is its upgrade layer. | Part | Job | |---|---| | WayneUI (project 0) | The interface layer every other part builds on | | CREED | The version policy (`policy.json`) Booster reads | | PROCTOR | Measures an app against that policy; the gate runs it before the first hop and after the last | | Preflight | The CI templates | | SESAME | Auth; Booster can swap `wayne-cas` for it | | HELMET | The baseline runtime | | Pinstripe | The coding standard | | Sourdough | The starters | | Booster | This project: moves apps forward | It works for all five permutations of the stack: raw PHP, raw PHP with a Vue SPA, Laravel with Blade, Laravel with Blade and Vue islands, and Laravel with Inertia and Vue. ### Install Run Booster from inside the app you want to upgrade: `--path` defaults to `.`, so `booster plan` needs no flags. Booster needs PHP 8.3 or newer on the machine; the app it upgrades does not, because Rector runs from Booster's own `vendor/`. #### Single file (PHAR) ``` mkdir -p ~/bin curl -L -o ~/bin/booster.phar https://github.com/waynestate/booster/releases/latest/download/booster.phar alias booster='php ~/bin/booster.phar' # put this line in ~/.bashrc or ~/.zshrc to keep it cd /path/to/your-app booster detect booster plan booster run --dry-run booster run ``` `detect` reads the app and prints its type and versions. `plan` lists the steps. `run --dry-run` previews the edits and changes nothing. `run` edits files and makes no commit. A real `run` also needs git, a clean git folder (commit or stash first) and a Docker daemon for its test gate. #### Linux, macOS and WSL (Ubuntu): clone it ``` sudo apt install php-cli php-xml php-mbstring php-zip php-curl unzip git composer ``` Ubuntu splits the XML extension out of `php-cli`, and Composer's dev dependencies need it: when Composer says "ext-dom is missing", run the line above. On macOS: `brew install php composer git`. Ubuntu 22.04 ships PHP 8.1, too old for Booster: add the [ondrej/php PPA](https://launchpad.net/+ppas?field.name=php&field.status=active) or use the Docker route below. Then: ``` git clone https://github.com/waynestate/booster.git cd booster && composer install bin/booster --version ``` Run Booster from its own folder with `--path` pointing at the app; never clone Booster inside an app: ``` bin/booster plan --path=/path/to/your-app ``` `--path` takes a path, and a leading `~` means your home folder (from `HOME`, or `USERPROFILE` on Windows). In WSL, keep Booster and the app in your Linux home folder, not under `/mnt/c` (a Windows drive is slow from Linux); a Windows folder is `/mnt/c/Users//...`, and `run` needs Docker Desktop's WSL integration switched on. #### Windows PowerShell, with PHP and Composer installed ``` git clone https://github.com/waynestate/booster.git cd booster; composer install php bin\booster --version php bin\booster plan --path=..\my-app ``` #### Docker only, no PHP (Windows or any OS) ``` git clone https://github.com/waynestate/booster.git cd booster docker run --rm -v "${PWD}:/booster" -w /booster composer:2.10.3 composer install --no-interaction docker run --rm -v "${PWD}:/booster" -v "/path/to/app:/app" -w /booster php:8.4-cli php bin/booster plan --path=/app ``` Run these from inside the booster folder. This route can run `detect`, `plan`, `scan` and `run --dry-run`. A real `run` starts Docker and uses git from inside Booster, which the `php:8.4-cli` image lacks, so for a real run use the single-file or clone route (in WSL on Windows). When Booster itself runs in a container, pass `--skip-gate` to `run` and run the tests yourself. Node hops run `npm` in a `node:26` container, so they need the same daemon but no Node installed on the machine. Booster prints its progress lines on standard error. In Windows PowerShell they print in red, as if they were errors. They are not errors; the exit code tells you whether the run worked. Booster is also on Packagist as `waynestate/booster`: give it its own folder, not your app's `vendor/`, because it needs PHP 8.3 even when your app doesn't. Clone it as above, or run `composer create-project waynestate/booster booster ^1.0`. ### Single-file download Booster ships as a single-file PHAR, so Preflight and apps without Composer access can run it as one file. The first route above downloads it; every version tag publishes the same `booster.phar`: the GitLab release carries it as an asset, and the GitHub release at github.com/waynestate/booster carries it as an attachment. Run it with `php booster.phar`. You can also build it from any commit, with the script for your OS (both need Docker, and no PHP or Composer on the host): Windows (PowerShell): ``` .\scripts\build-phar.ps1 ``` Linux / macOS: ``` scripts/build-phar.sh ``` Each script writes `build/booster.phar` and verifies it by printing Booster's version. Every command works from the phar. `detect`, `plan`, `hops` and `init` read only what is bundled inside it. `run` copies Rector, its `config/rector.php` and `docker/` out of the phar into a temp directory first, because Rector and `docker build` are child processes that cannot be pointed at a `phar://` path; the temp directory is removed when the command exits. ### Quick start ``` booster detect --path=../my-app # which of the five permutations, and which versions booster plan --path=../my-app # the ordered hops to a supported version booster scan --path=../my-app --php= # what will break on that newer PHP; advisory, changes nothing booster run --path=../my-app --dry-run # the diffs Rector would make; changes nothing booster run --path=../my-app # apply the plan, one hop at a time, with a test gate booster init --path=../my-app # optional: writes playbooks for AI coding tools into the app; commit them before a real run ``` Replace `` with the PHP version you are heading for, for example the one `plan` shows on its last step. A real run needs git and Docker on the machine that runs Booster. If you used the Docker-only route, the first four commands above work as they are, but the last one does not: its `php:8.4-cli` image has no git. Use the single-file or clone route for `booster run` (see Install). Booster prints its progress lines on standard error. In Windows PowerShell they print in red, as if they were errors. They are not errors; the exit code tells you whether the run worked. #### What a run does by default You can run `detect`, `plan` and `run` on an app with nothing after `--path`. Booster then goes as far as it safely can on its own: it climbs to the newest PHP and Laravel that CREED's policy supports today, which is PHP 8.5 and Laravel 13 as of this writing (the policy file is the source of truth, not this page). | Command | With no other flags | | --- | --- | | `booster detect` | Reads `composer.json` and `composer.lock`, and reports PHP, Laravel, PHPUnit and the front-end setup. Changes nothing. | | `booster plan` | Starts from what `detect` found and plans every hop up to the newest PHP, Laravel and PHPUnit the policy supports. Changes nothing. | | `booster run` | Does the whole plan in one go, in order. | How many hops? One for every step on the way, never a jump: - one hop per PHP minor (8.1 to 8.2 is a hop, 8.2 to 8.3 is another), - one hop per Laravel major, - one hop per PHPUnit major. Booster interleaves them so the app sits on a PHP and Laravel pair that Laravel supports at every step: a Laravel step as soon as the current PHP allows it, a PHPUnit step when the PHP allows it, a PHP step otherwise. Two real examples: a Laravel 10 app on PHP 8.1 plans 11 hops, and a PHP 5.5 app with no framework plans 12 hops to PHP 8.5. Some hops come along by default, and some only when you ask: - The front-end Node hops (Mix to Vite, Tailwind, Vue) are in the plan by default. Pass any target (`--goal`, `--to-php`, `--to-laravel`, `--to-phpunit`, `--from-php`, `--from-laravel`) and they drop out with a warning, unless you add `--with-node`. - An app on PHPUnit 8 or older gets a manual PHPUnit 9 step before the first PHP 8 hop. - The SESAME move (`--sesame`) and the WayneUI move (`--wayneui`) are opt-in only. - `--skip ` drops a hop you do not want. - If a target is past security support, the plan stops at the policy limit and warns. If Rector has no ruleset for a step yet, the plan stops short and warns. `run` handles each hop the same way: a Rector preview, then the edits (if a hop would change more than 200 files it asks first, or stops when it cannot ask; `--max-changes` sets a hard limit), then the Composer constraints, then your tests in the target PHP image. That last part is the gate. The first hop that fails stops the run, and the hops after it are reported as "not run". Booster never commits; the changes sit in your working copy for you to review. Progress prints one line per hop, like `hop 3/11 : , 12 files, 41 s`, and ends with `run finished: 11 of 11 hops run, 0 stopped, ...`. Looking for a specific situation (only PHP, one hop at a time, backend only, a big app, no tests)? See [How do I...](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/docs/recipes.md). Want less than the whole climb (we will not judge, it is a long way up)? - `--dry-run` previews every hop and changes nothing. - `--until ` keeps that hop and everything before it. - `--only ` runs just that one hop. - `--to-php` and `--to-laravel` pick a lower target. The one-command form for a forced image upgrade: ``` booster run --path=../my-app --goal=php-74-to-84 ``` When the app carries a `composer.lock`, `detect` also reads the PHP range the locked packages allow: the highest `require.php` floor and the lowest ceiling across `packages` and `packages-dev`, raised by a `platform-overrides.php` or `config.platform.php` pin in `composer.json` when one is set. If that floor sits above the floor `composer.json` claims, Booster plans from the lock's floor and prints a warning saying so; if the first PHP hop lands at or above the lock's ceiling, a second warning notes that hop will need a `composer update` to keep the lock installable. Without a lock, `composer.json` stays the sole source. A plan for the bundled Laravel 10 example app on PHP 8.2 looks like this (the order keeps the app on a PHP and Laravel pair Laravel supports at every step): ``` $ booster plan --path=examples/laravel-blade --today=2026-10-29 BOOSTER plan for examples/laravel-blade app type: laravel-blade PHP: 8.2 -> 8.5 Laravel: 10 -> 13 PHPUnit: 10 -> 13 policy: waynestate/creed 0.1.4 (today 2026-10-29) 1. phpunit-10-to-11 Move PHPUnit 10 tests to PHPUnit 11. After this step your tests run in Docker. 2. laravel-10-to-11 Move a Laravel 10 app to Laravel 11. After this step your tests run in Docker. 3. laravel-11-to-12 Move a Laravel 11 app to Laravel 12. After this step your tests run in Docker. Behavior changes to check: When the app does not define a local disk, its root now defaults to storage/app/private instead of storage/app (config/filesystems.php) keep old: define the local disk with 'root' => storage_path('app') check: Check where Storage::disk('local') writes after the hop 4. php-82-to-83 Move PHP 8.2 code to PHP 8.3. After this step your tests run in Docker. Removed between PHP 8.2 and PHP 8.3: none found. 5. phpunit-11-to-12 Move PHPUnit 11 tests to PHPUnit 12. After this step your tests run in Docker. 6. laravel-12-to-13 Move a Laravel 12 app to Laravel 13. After this step your tests run in Docker. 7. php-83-to-84 Move PHP 8.3 code to PHP 8.4. After this step your tests run in Docker. Removed between PHP 8.3 and PHP 8.4: none found. 8. phpunit-12-to-13 Move PHPUnit 12 tests to PHPUnit 13. After this step your tests run in Docker. 9. php-84-to-85 Move PHP 8.4 code to PHP 8.5. After this step your tests run in Docker. Removed between PHP 8.4 and PHP 8.5: none found. PHPCompatibility check for PHP 8.3: no findings. PHPStan on PHP 8.5: 5 findings (code quality hints, not upgrade blockers). app/Models/Post.php:7 [class.notFound] Class App\Models\Post extends unknown class Illuminate\Database\Eloquent\Model. app/Models/Post.php:9 [missingType.property] Property App\Models\Post::$casts has no type specified. tests/PostTest.php:18 [property.onlyWritten] Property App\Tests\PostTest::$repository is never read, only written. tests/PostTest.php:27 [staticMethod.deprecated] Call to deprecated method isType() of class PHPUnit\Framework\Assert: https://github.com/sebastianbergmann/phpunit/issues/6052 tests/PostTest.php:33 [method.alreadyNarrowedType] Call to method PHPUnit\Framework\Assert::assertNotNull() with PHPUnit\Framework\MockObject\Rule\AnyInvokedCount will always evaluate to true. Hint: Laravel app without Larastan: Eloquent and facade calls show as errors. composer require --dev larastan/larastan for a cleaner scan. Nothing was changed. The numbered list above is the order Booster would do things; "PHPCompatibility check" and "PHPStan" are advice for you to read, not errors. Booster does every numbered step above when you run it. Preview the edits: booster run --path=examples/laravel-blade --dry-run (The real run needs Docker, and your app committed in git: booster run --path=examples/laravel-blade) ``` ### Commands | Command | What it does | |---|---| | `booster detect` | Reads `composer.json` and `package.json`; prints the app type, PHP, Laravel, PHPUnit, Vue, Node and Boost | | `booster plan` | Prints the ordered hops, the warnings (including which direct dependencies block which hops), a removals line per PHP hop, a behavior-changes block per PHP and Laravel hop, an advisory PHPCompatibility scan of the first PHP hop's target and a PHPStan scan at the destination PHP. Changes nothing. | | `booster scan` | Advisory scans of one PHP version: PHPCompatibility by default, `--removals` for removed functions, `--phpstan` for static analysis. Exits 0 even with findings | | `booster run` | Runs the plan. `--dry-run` previews every hop | | `booster hops` | Lists every hop Booster knows | | `booster init` | Writes the coding-tab playbooks to `.agents/skills`; `--check` exits 1 when they are stale | Options shared by `plan` and `run`: | Option | Meaning | |---|---| | `--path` | The app (default `.`); a leading `~` means your home folder | | `--format` | `text` (default) or `json` | | `--policy` | CREED's `policy.json`. Default: `BOOSTER_POLICY`, then the copy in the `waynestate/creed` package | | `--today` | Override today's date (`YYYY-MM-DD`) so runs are repeatable | | `--goal` | Shorthand such as `php-74-to-84` (sets `--from-php` and `--to-php`) | | `--from-php`, `--from-laravel` | Say what the app runs on when the manifest does not | | `--to-php`, `--to-laravel`, `--to-phpunit` | The target. Default: the newest version the policy supports today (PHPUnit: the newest that runs on the target PHP) | | `--sesame` | Include the `wayne-cas` to SESAME swap | | `--wayneui` | Include the WayneUI 1.x to 2.x move | | `--skip` | A hop id to leave out (repeat it) | | `--no-package-check` | Skip the Packagist dependency check (for offline use) | | `--until` | Cut the plan after this hop id: that step and everything before it stay, and the plan's targets move to where it lands | | `--only` | Plan and run just this one hop id. The app must sit on the hop's starting versions, and `--until` and `--only` are never combined | | `--with-node` | include the Node front-end hops (Node pin, Mix to Vite, Tailwind, Vue) even when you passed a target version. Without it, any of `--goal`, `--to-php`, `--to-laravel`, `--to-phpunit`, `--from-php` or `--from-laravel` leaves those hops out, and the plan prints: "Node front-end hops were left out because you passed a target version: . Add --with-node to include them." | Example: an app moving from Laravel 8 to 13 takes the climb in parts. `booster plan --path=../my-app --until=laravel-8-to-9` keeps every hop up to and including the move to Laravel 9, and `booster run` with the same flag applies exactly those hops. Options for `plan`: | Option | Meaning | |---|---| | `--no-scan` | Skip the advisory scans (PHPCompatibility and PHPStan) | Options for `scan`: | Option | Meaning | |---|---| | `--path` | The app (default `.`); a leading `~` means your home folder | | `--php` | Required. The PHP version to ask about (`major.minor`, one Booster knows) | | `--removals` | List uses of functions and constructs removed in PHP up to `--php`, instead of the PHPCompatibility scan | | `--phpstan` | Also run the PHPStan scan (Pinstripe preset) at the same PHP version | | `--format` | `text` (default) or `json` | Options for `run`: | Option | Meaning | |---|---| | `--dry-run` | Show the diffs; change nothing; skip git, the baseline and the gate. A Laravel hop without `vendor/` is marked incomplete | | `--max-changes` | Hard limit on files changed per hop, with no prompt (`0` lifts the limit). Without it, a hop over the review threshold of 200 files asks first when interactive, or stops | | `--memory-limit` | Memory limit for the Rector process (for example `2G`), or `-1` for no limit (default `2G`) | | `--skip-gate` | Do not run the tests in the target image after each hop, or the PROCTOR check at the end (skips the baseline too) | | `--image` | Run the gate in this Docker image instead of building `booster-target` (it needs `php` and `composer` on `PATH`): one name, a `{php}` template or a version map (below) | | `--allow-failing-baseline` | Continue when the baseline fails; later gate failures are labeled as pre-existing | | `--allow-dirty` | Run even when the working copy has uncommitted changes | | `--test-command` | What runs the app's tests in the image (default `composer test`, else `vendor/bin/phpunit`, else the built-in `php -l` syntax check when the app has no tests) | | `--test-env` | Environment variable (`KEY=VALUE`) passed to the baseline and every gate container with `-e`; repeat it. The report names the keys only, never the values | | `--proctor-command` | An extra command run in the image after the tests, for example a PROCTOR check | | `--proctor-advisory` | Report the PROCTOR result but do not fail the run on it. Applied automatically when the run stops before the final target (`--until`, `--only`, or a target past security support) | Exit codes: `0` pass, `1` the baseline failed, a hop was capped, errored or failed its gate, or the PROCTOR check found new findings, `2` bad usage (bad option, unreadable policy, broken `composer.json`, a target the policy does not know). #### The PHPCompatibility scan `plan` ends with an advisory scan: PHP_CodeSniffer runs the PHPCompatibility standard over the app and asks what will break on the PHP version the first PHP hop lands on. The findings are hints for the person doing the upgrade, never a gate: they do not change the exit code, and a scan that cannot run (missing tooling, a crash) is reported as "could not run" while the plan still prints in full. `--no-scan` skips both advisory scans. The scan needs no change to the app: Booster ships PHP_CodeSniffer and the PHPCompatibility standard with its own dependencies, from the PHAR and from a clone alike. Booster looks for a PHP_CodeSniffer that carries the standard, trying the app's own `vendor/bin/phpcs`, then Booster's `vendor/bin/phpcs`, then `phpcs` on the PATH. When it runs Booster's own copy, it points PHP_CodeSniffer at the bundled standard with `--runtime-set installed_paths`, because the registration file that Composer's installer plugin writes cannot be trusted inside a PHAR unpacked into a temp directory. When none of the launchers has the standard, the scan reports a skip and the plan goes on; the skip says to reinstall Booster's dependencies (`composer install` in Booster's folder) or download the PHAR again. The scan ignores `vendor/`, `node_modules/` and `storage/`, because those hold third-party code and generated files, not the app's. It reads the app only and changes nothing. `booster scan --path=../my-app --php=` runs the same scan on its own, for any PHP version Booster knows, and exits 0 even when it finds a wall of removed functions. If the app's `composer.json` declares `ext-oci8` (in `require`, `require-dev` or `config.platform`), the scan leaves out the "`oci_*()` is removed since PHP 8.4" findings: PHP 8.4 moved oci8 out of the PHP download and into PECL, and an app that declares the extension installs the PECL one on purpose, so those lines would be false alarms. Every other finding stays. #### The PHPStan scan `plan` ends with a second advisory scan: PHPStan runs over the app at the destination PHP version, using Pinstripe's `base.neon` preset. The findings are hints for the person doing the upgrade, never a gate: they do not change the exit code, and a scan that cannot run is reported with the fix that enables it, while the plan still prints in full. `--no-scan` skips it. The scan needs no change to the app: Booster ships PHPStan and Pinstripe's preset with its own dependencies, from the PHAR and from a clone alike. Booster looks for a PHPStan binary, trying the app's own `vendor/bin/phpstan`, then Booster's `vendor/bin/phpstan`, then `phpstan` on the PATH, and for the preset it tries the app's vendor tree and Booster's. When either is missing, the report says to reinstall Booster's dependencies (`composer install` in Booster's folder) or download the PHAR again. It ignores `vendor/`, `node_modules/` and `storage/`, reads the app only and changes nothing. `booster scan --path=../my-app --php=8.5 --phpstan` runs the same scan on its own, for any PHP version Booster knows, and exits 0 even when it finds a wall of missing types. #### The dependency check Before the hops print, `plan` (and `run`) asks Packagist about each direct third-party dependency in the app's `composer.json`: what is its newest stable release, and can that release still be installed at each hop's destination? Only plain `require` and `require-dev` entries count; PHP, extensions (`ext-*`, `lib-*`), Laravel itself (`laravel/framework`, its `illuminate/*` components), PHPUnit and Booster's own scan tools are Booster's own business, so they are skipped. The answer is advisory, like the scans: a package that cannot follow the plan does not drop the hop. It adds a warning that names the package, the hop it blocks, and what to do about it: ``` acme/legacy-auth 2.3.0 supports Laravel up to 10 and PHP up to 8.3, so it blocks laravel-10-to-11. Replace or upgrade it first. ``` The warning names only the dimensions the blocked hop fails on: a Laravel ceiling alone, a PHP ceiling alone, or both together. A ceiling is printed as the newest version Booster plans that the constraint allows (a PHP constraint capping at 7.x reads "7.4", not "7"). When the constraint has no upper bound, or when the hop's target is below the constraint's floor, the warning names the floor instead (`needs PHP 8.2 or newer`). A package whose newest release requires several `illuminate/*` components is judged by the loosest of those constraints, the one that allows the most Laravel versions. Abandoned packages get their own warning (`acme/old is abandoned (use acme/new instead).`). Packages the check cannot reach are reported as `Not checked: (not on Packagist or unreachable).` or `(VCS repository).`, and the plan goes on. In JSON output every package lands under `package_checks`, with the fields `package`, `newest`, `blocks`, `abandoned` and `checked`. The check fetches from `repo.packagist.org` and from any composer-type repository the app declares in `composer.json`; VCS repositories are never fetched. `--no-package-check` skips the whole check, for offline use. #### Behavior changes Some changes between versions do not raise an error: a function that still exists now means something else, a config key that was optional is required, a default that flipped. Rector rewrites what it knows; the silent changes need a human eye. So `plan` and `run` carry a curated list of them, one block per PHP and Laravel hop, plus the package rows on the last step, where the lock lands. A booster should know its own payload. ``` Behavior changes to check: Config key needed (config/app.php) check: Add the key. Minimum PHP raised check: Check the runtime. ``` Each entry says what changed (`label`), what keeps the old behavior when there is anything (`keep old:`), what to verify (`check:`), and where the app touches it: `file:line` hits for code, the file for config and package rows, no location for rows that apply everywhere. It is advisory, like the scans: it never changes the exit code, and nothing prints when nothing matches. In JSON output the entries land under `behaviour_changes`, per step, with the fields `id`, `label`, `keep_old`, `check`, `source`, `hits` and `total`. The list lives in `data/behaviour-changes.json`, bundled with the phar. An entry names the hop it applies to (`php-80-to-81`, `laravel-10-to-11`, or `package:vendor/name@2` for a floor a package raises), how to find it (`detect`: a `function`, `method`, `class`, `property`, string `token`, blade snippet, config `file` and `key`, composer `package`, or `always`), and a `source` link that confirms the change: ```json { "id": "laravel-11-config-key", "hop": "laravel-10-to-11", "detect": { "kind": "config", "file": "config/app.php", "key": "cipher" }, "label": "Config key needed", "keep_old": null, "check": "Add the key.", "source": "https://laravel.com/docs/11.x/upgrading" } ``` Only confirmed changes ship, each with its source link, and at most three `always` rows per release, so the block stays a checklist rather than a pamphlet. To add an entry, append it to the data file and let the loader's errors tell you what is missing. #### The baseline Before the first hop, `run` runs the app's own tests (or the built-in `php -l` syntax check when the app has no tests) on the PHP the app is on now, in the same `booster-target:` image the gate uses. The point is to tell "Booster broke it" from "it was already broken": a red baseline means the suite was red before any change, so a later red gate is not proof of a bad hop. The baseline is recorded in the report (`passed`, `failed` or `skipped`, with the exit code and the last 20 lines of output). - A red baseline stops the run before the first hop, with a non-zero exit. `--allow-failing-baseline` lets it go on; each later gate failure is then labeled "the baseline was already failing" in the text and JSON output. - `--skip-gate` and `--dry-run` skip the baseline (reported as `skipped`), so a dry run never starts Docker. - A Mix app's `public/mix-manifest.json` is a build artifact, not committed source: a fresh clone has no manifest, so the suite can be red before any hop. Build the assets before the baseline (the app's usual `npm ci && npm run build`), or pass `--allow-failing-baseline` knowing why. - When the app has no tests of its own, the baseline and every gate run a built-in check instead: every PHP file outside `vendor/` and `node_modules/` must still parse on the target PHP (`php -l`). That proves the code loads on the new PHP, not that it works: click through the app before you ship. Pass `--test-command` to run something else, or add a smoke test (below) to get the real gate back. #### A smoke test for WSU apps Laravel's stock tests do not fit a WSU app. `tests/Feature/ExampleTest.php` asks for `/` and expects 200, but on a real app the front page sits behind CAS, so an unauthenticated request is a redirect to the sign-in URL, never a 200. The stock suite also assumes an `APP_URL` that points somewhere real and an `APP_KEY` that exists; a fresh checkout has neither, so the suite is red before any change, and even a green run of it proves nothing about the upgrade. When Booster sees only the two stock example tests, it says so next to the baseline: The app's tests are only Laravel's stock examples, so the gate cannot prove anything. See "A smoke test for WSU apps" in the README. Write one small test that proves the app boots and its front door still works. It needs a few environment values in `phpunit.xml`, because the test environment starts empty: ```xml ``` `APP_KEY` can be any throwaway 32-byte key (`php artisan key:generate --show`); it never leaves the test environment. If the app touches the database while booting, point the tests at an in-memory SQLite database: ```xml ``` And the test itself, in `tests/Feature/SmokeTest.php`: ```php get('/'); $response->assertRedirect(); $this->assertStringContainsString('/login', (string) $response->headers->get('Location')); } } ``` That is the whole point: the app boots, encryption and sessions work, and the front door still sends guests to CAS. It passes on the old PHP and must still pass on the new one, which is what the gate is for. Those values live in `phpunit.xml`, committed with the app. When you would rather not commit them, pass them on the command line instead; Booster forwards each one to the baseline and every gate container with `-e`, and the report names the keys only, never the values: booster run --path= --test-env DB_CONNECTION=sqlite --test-env DB_DATABASE=:memory: #### What a hop does On PHP hops a pre-pass runs first: PHP 4 era `$a = &new Foo();` becomes a plain assignment, because PHP 7 and newer refuse to compile the old spelling and Rector would never get a parse tree. The report lists the rewrites per file under `Pre-pass fixes:`. 1. Rector runs as a dry run. If it reports errors, the run stops before anything is written. So does a hop that would change more than 200 files, unless an interactive run confirms it after showing the per-rule summary; an explicit `--max-changes` is a hard limit that never asks. An empty Rector report usually means the process ran out of memory, and the message points at `--memory-limit`. 2. Rector runs for real, with only that hop's rules. A rule that would change what the code does is never applied: the PHP 8.0 set's switch-to-match rewrite is skipped, because `match` compares strictly and would stop a `case 1:` from matching the string `"1"` a query string carries. Every run keeps Rector's and PHPStan's caches in its own temp directory, removed at exit, so one run never reads another's. 3. `composer.json` is edited: the `php` or `laravel/framework` constraint is raised, and `config.platform.php` moves with it when the app already pins it. Booster never creates that key. 4. The gate runs the app's tests inside `booster-target:-`, a container built from `docker/Dockerfile` on the PHP the hop moves to. The stamp is a short hash of the recipe, so an edited `docker/Dockerfile` rebuilds the image instead of reusing a stale one. The image carries the extension set Citadel's apps run with: intl, ldap, soap, zip, gd, pdo_mysql, mysqli, bcmath, pcntl, exif, redis, imagick and oci8. The first build downloads Oracle Instant Client for oci8, so it takes a few minutes; later hops reuse the image. Pass `--image` to run the gate in your own image instead (below). A red gate stops the run and leaves the changes in place so you can read them (`git checkout .` undoes them). To check the image itself, `scripts/check-target-image.sh` (or its Windows twin `check-target-image.ps1`) builds it for each PHP version and verifies its extension list; it needs Docker and a few minutes per version, so it is not part of `composer ci`. 5. On Laravel and PHPUnit hops the gate refreshes the lock before the tests: `composer update -W` for the package the hop moved, so `composer.lock` lands on the new major version next to `composer.json`. Every other hop, and the baseline, run a plain `composer install`. The two phases report separately: a Composer failure names the Composer phase and Composer's own output, because the tests never ran. Composer 2.10 refuses to load tagged releases Packagist has flagged with a security advisory, and hops that move onto Laravel 9 or 10 most often meet that block. The refusal has two faces. The loud one fails the install phase and names the advisories. The quiet one happens when the app allows dev stability: Composer does not fail at all, it locks the branch instead, `9.x-dev` where the hop asks for `^9.0`. Booster reads the refreshed lock back and treats a branch version behind a tagged range like the block. Either way it writes a temporary entry to `config.policy.advisories.ignore` in `composer.json`: scoped to the package and the constraint Composer refused, kept visible to `composer audit`, and carrying a reason PROCTOR can find and report until the app removes it. The gate then retries once. If the retry still fails, the run stops with the unblock and the failure both in the report, and the entry stays in `composer.json`. The unblock is temporary by name, and Booster enforces that. When a gated step passes, Booster re-reads `config.policy.advisories.ignore` and reports every entry whose reason carries its own prefix (`Booster temporary unblock`; older runs wrote `BOOSTER temporary unblock`, which it still recognizes). A step that is not the plan's last one applies with a warning; the plan's last step fails until the entry is out of `composer.json`, so the final target never ships with the unblock still in place. Removing the entry is the app's job once it is off the blocked versions. The `--image` value names the image the baseline and every gate run in, and it takes three shapes: - One name, used for the baseline and every gate: `--image myapp/gate:8.4`. - A template whose `{php}` is replaced with the PHP version of that baseline or gate: `--image registry/php-apache:{php}` gates the 8.1 hop on `registry/php-apache:8.1` and the 8.2 hop on `registry/php-apache:8.2`. - A map of PHP version to image, for a fleet whose images do not follow one pattern: `--image 8.0=legacy/php:7.4,8.1=registry/php-apache:8.1`. A map without the version a baseline or hop lands on stops the run before the first hop and names the fix; a template never misses. Booster never builds a `--image` image: it must exist locally or be pullable, and each resolved image is pulled at most once per run. On an arm64 Mac, Citadel's published images such as `citadel/php-apache:8.5r3` are amd64 only: pass an arm64 build via `--image`, or set `DOCKER_DEFAULT_PLATFORM=linux/amd64` and let Docker emulate. `booster-target` itself builds natively on arm64. And the old-PHP images (5.5 through 8.0) point apt at archive.debian.org, so they get no security updates: they exist to run the app's tests on the way up, not to serve traffic. `run` refuses to start on a working copy that is not under git or has uncommitted changes, so the upgrade diff stands alone. `--dry-run` has no such limit. A dry run previews every hop against the original tree, so the same file can appear in several hops. The report ends with the number of unique files and a reminder that nothing was applied. A Laravel hop whose app has no `vendor/` cannot see the framework's classes, so its preview is marked incomplete and the report says to run `composer install` first. A run prints one line per hop to standard error while it works: the hop's position in the plan, its id, its status, the number of files it changed and the seconds it took, and a final line totals the hops run, the stops and the whole run. They go to standard error so the report on standard output is untouched and `--format json` stays valid JSON. Three hops edit manifests instead of code. `node-pin-24` sets `engines.node` in `package.json` and writes `.nvmrc`, then the app's usual gate applies. `mix-to-vite` converts a simple `webpack.mix.js` to Vite (below) and asks for a build gate, which runs `npm ci` and `npm run build` in the `node:26` container before the app's tests. `tailwind-3-to-4` runs the official `@tailwindcss/upgrade` tool in the Node container, then pins `tailwindcss` to `^4.1`, adds the matching Vite or PostCSS plugin, and removes `autoprefixer` and `postcss-import`, which Tailwind 4 folds in; plugins listed in `tailwind.config.js` and `@apply` inside Vue SFC `` blocks stay manual work, and the hop says so. Vue 2 to Vue 3 is staged. `vue-26-to-27` moves the front end to the last Vue 2 release, which carries the Vue 3 reactivity API; `vue-27-to-compat` switches to the `@vue/compat` build; `vue-compat-codemod` runs the `vue-upgrade-tool` codemod and keeps every manual item it lists. `vue-2-to-3` is the playbook that stands in for all three: skipping it skips them, and a person finishes the job, because Rector does not read Vue. #### Hops Booster knows Thirty-four hops: PHP one minor at a time from 5.5 to 8.5, Laravel one major at a time from 5.8 to 13 and PHPUnit one major at a time from 9 to 13, each list read from `data/versions.json` and trimmed to the majors whose Rector ruleset is installed ([Hop data](#hop-data)). An app whose PHPUnit is below 9 gets `phpunit-to-9-manual`, a manual move to `^9.5` placed before the first hop that targets PHP 8.0 (PHPUnit 7 and 8 do not run on PHP 8), and the regular PHPUnit hops then take over from 9. Then the recipes: `cas-to-sesame` (writes `config/sesame.php` from the old `wayne-cas` config, or from SESAME's published defaults when the app has none, moves the CAS middleware from the web group onto the app's web routes, points the web guard at the `sesame` driver and the users provider at `eloquent` (adding a model key when it has none), rewrites user models that use the `WayneCasable` trait to implement SESAME's compatibility interface, swaps the Composer requirement and drops any `wayne-cas` path or VCS repository entries, removes `config/wayne-cas.php` when nothing in the app reads its config keys any more (a manual step names the readers while anything still does), and flags dead `DUO_*` settings in example env files; planned with `--sesame`, and left out with a warning when the plan ends below PHP 8.3, which SESAME needs; it lands just before the hop from Laravel 10 to 11, and while PHP is still below 8.3 the PHP hops go first); Booster also probes the repositories composer.json names for wayne-cas (a path entry must exist on disk, a vcs, git or gitlab entry is checked with `git ls-remote`), and when the app has no `vendor/` and that repository cannot be reached the swap goes first instead, because the Laravel hops need composer to install and composer is blocked by that repository; this applies to an app already on PHP 8.3 or newer (SESAME needs 8.3, so below that the PHP hops still go first); the plan says so in a warning; `wayneui-1-to-2` (planned with `--wayneui` for an app that requires `cit-web-infra/wayne-ui`, after the last Laravel hop and before the Vue hops, on Laravel 11 or newer: swaps the Composer requirement for `waynestate/wayne-ui ^2.0`, rewrites the one-to-one rows (class tokens, icon names, the mapped brand hexes in style blocks and CSS, the `Breadcrumbs` calls and the `config/wayne-ui.php` keys) and lists the rest as manual lines; it leaves forked views under `resources/views/vendor/wayne-ui` and `public/vendor/wayne-ui` in place for a person to delete, and it cannot rotate a deploy token a repository URL may carry); `node-pin-24` (planned for any app with a `package.json` that does not already floor Node at the hop data's target, 26 today: sets `engines.node` to `>=26` and writes `.nvmrc`, ahead of the front-end hops); `mix-to-vite` (planned when the app has a `webpack.mix.js` and the plan lands on Laravel 9 or newer, after `node-pin-24`: writes `vite.config.js`, swaps `laravel-mix` for `vite` and `laravel-vite-plugin` in `package.json`, renames `MIX_` keys to `VITE_` in `.env.example`, rewrites Blade `mix()` script and link tags into one `@vite([...])` line per layout when every referenced asset maps to a Mix entry, and deletes `webpack.mix.js`; the simple shape only: `mix.js`, `.vue()`, `.sass()`, `.postCss()`, `.version()` and `if (mix.inProduction()) { mix.version(); }` (dropped with a note, since Vite hashes production assets), anything else is refused by name, a Vue 2 front end must take `vue-2-to-3` first, and `withoutMix()` calls are listed for a manual rename to `withoutVite()`); `tailwind-3-to-4` (planned when `package.json` carries `tailwindcss` below 4, after `node-pin-24` and `mix-to-vite`: runs the official `@tailwindcss/upgrade` tool, pins `tailwindcss` to `^4.1`, adds the matching Vite or PostCSS plugin and removes `autoprefixer` and `postcss-import`); and the Vue 2 to Vue 3 stage, `vue-26-to-27`, `vue-27-to-compat` and `vue-compat-codemod`, with the `vue-2-to-3` playbook standing in for all three. `booster hops` prints them all. > Every other hop lifts the engine; `wayneui-1-to-2` is the one that repaints the bodywork. Booster plans any target the policy knows. A target past security support is allowed as a stop on the way up, with a plan warning that names the date its security support ended. A version the policy does not list at all is still refused by name. ### Configuration | Variable | Meaning | Default | |---|---|---| | `BOOSTER_POLICY` | Path to CREED's `policy.json` | the copy in the `waynestate/creed` package | Booster reads the policy straight from the `waynestate/creed` package in its own `vendor/`, so there is no copy to drift: `composer update waynestate/creed` is the whole refresh. `--policy` and `BOOSTER_POLICY` still override it for one run. `.env.example` holds the placeholder. Booster reads no secrets, makes no network calls of its own and talks to no Citadel host. Everything it prints from an app (file names, diffs) has control characters stripped. When Rector runs, Booster passes it a few `BOOSTER_*` variables of its own (the app path, the sets, the PHP target). They are internal; do not set them. ### Laravel hops and `vendor/` rector-laravel needs Laravel's classes to recognize a model or a controller. For a Laravel hop Booster loads the app's own `vendor/autoload.php` when it exists, so run `composer install` in the app first. PHP hops never load the app's autoloader. An app with no `vendor/` still gets a plan and the hops that do not need Laravel's classes. For an Inertia/Vue app, each Laravel hop also raises the Inertia constraints the app already carries: `inertiajs/inertia-laravel` and `@inertiajs/vue3` move to the constraint the hop data pairs with the target major (`^2.0` for Laravel 11 and newer). Booster never adds a package the app does not have, and below Laravel 11 it pins nothing and says so, because the hop data carries no constraint there. ### Hop data `data/versions.json` holds the version facts Booster plans against: the PHP, Laravel and PHPUnit chains, the PHP range each Laravel major supports, the PHPUnit floor each Laravel major ships, the Node floor the `node-pin-24` hop writes, and the fixed Tailwind pin and Inertia constraint tables the front-end hops use. The file ships inside the PHAR. A new major appears in the file as soon as its feed says so; Booster offers a hop for it only once Rector ships a ruleset for it, and a plan toward a major without one stops there with a warning instead of failing. `bin/refresh-hop-data.php` rewrites the file from the public feeds (Packagist for PHPUnit and Laravel, endoflife.date for Laravel's supported PHP ranges, npm for the Vite packages) plus CREED's policy for what counts as supported: ``` php bin/refresh-hop-data.php # fetch the feeds, rewrite the file, print what changed php bin/refresh-hop-data.php --check # offline CI check: validate the shape, then compare the frozen feeds php bin/refresh-hop-data.php --online # compare against the live feeds without writing the file php bin/refresh-hop-data.php --offline tests/fixtures/hop-feeds --today 2026-10-29 --dry-run ``` The refresh is deterministic for a given set of feeds and date, and it never touches the recipes: writing a playbook for a new hop stays human work. `composer ci` ends with the offline `--check`: it validates the file's shape against the bundled policy, then recomputes it from the frozen feeds in `tests/fixtures/hop-feeds` as of the file's `generated_on` date, so the check itself needs no network. A scheduled pipeline runs the hop data robot, `bin/hop-data-bot.php`, against the live feeds; when a feed moved while the file slept, the robot opens (or updates) a merge request on Booster itself with the new file, and a person merges it. [MAINTAINING.md](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/MAINTAINING.md) covers the token the robot needs. ### Deploying on Citadel When a real (not dry) run changes `composer.lock` and the app has a `deployments/` directory, Booster writes one deployment script, `deployments/0000_composer_install_`: ```sh #!/bin/sh # Added by Booster: this upgrade changed composer.lock. Citadel runs each deployment script once, in name order, # so a new name makes existing deployments install the new vendor/ before anything else runs. set -e composer install --no-dev --no-interaction --optimize-autoloader php artisan migrate --force ``` Citadel runs every `deployments/` script exactly once, in name order, and the leading `0000` puts this one first. So when the app next deploys, every existing deployment installs the upgraded `vendor/` before any other script runs. The `php artisan migrate --force` line appears only for apps that have an `artisan` file; raw PHP apps get the `composer install` alone. Commit the script with the rest of the upgrade. To drop it again, delete the named file. ### Checks Maintainers: how to build, test and release this project, and where its data comes from, is in [MAINTAINING.md](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/MAINTAINING.md). ``` composer ci ``` runs `composer validate --strict`, PHPUnit, Pint, PHPStan (Pinstripe's raw preset, inlined; `waynestate/pinstripe` ships with Booster and supplies the preset the advisory scan uses), `composer audit`, the hop data check and the agent-ready check. Nothing needs PHP on the host: ``` docker run --rm -v "${PWD}:/app" -w /app composer:2.10.3 sh -c "composer install --no-interaction && composer ci" ``` `contract/expectations.json` says what Booster must detect, plan and change for each example app in `examples/`; `tests/ContractTest.php` enforces it with the real Rector, so a Rector release that changes a hop shows up in CI. ### Proof Proof runs against real Wayne State applications are on a private site, [proof.apps.wayne.edu/booster](https://proof.apps.wayne.edu/booster/), behind a Wayne State employee sign-in. The [Real output page](https://booster.apps.wayne.edu/proof.html) shows `booster plan` and `booster run --dry-run` on each example app, captured from the real tool and rebuilt with `composer proof`; `composer ci` fails when it is stale. Proven how: the first full run with the gate on took a Laravel 12 app from PHP 8.2 to 8.4 in two hops, each gated by the app's 197-test suite, and it is written up in [research/booster-e2e-gate.md](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/research/booster-e2e-gate.md). ### Status Version 1.8. See [CHANGELOG.md](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/CHANGELOG.md) for what changed, [OPEN_QUESTIONS.md](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/OPEN_QUESTIONS.md) for what stays open, and [BEST_PRACTICES.md](https://nougat.snu.gs/enterprise-apps/booster/blob/HEAD/BEST_PRACTICES.md) for the rules Booster follows. ### License MIT, Wayne State University, Enterprise Applications. --- Source: https://booster.apps.wayne.edu/ PHP and Laravel upgrades · WELD stack ## BOOSTER Boosts your versions. A booster shot keeps your immunity current. This one keeps your PHP current, and it stings less. Booster works out the hops from the PHP or Laravel version your app runs today to one CREED still supports, and runs them one at a time. [Rector](https://getrector.com) rewrites the code and Booster raises the constraints in `composer.json`. Your tests have to pass on the new PHP before the next hop starts. - **One hop** per dose - **200 files** review threshold per hop - **0** commits made for you [Watch a plan launch](https://booster.apps.wayne.edu/#record) [Install](https://booster.apps.wayne.edu/#install) **Works alongside [WELD](https://weld.apps.wayne.edu/).** WELD and [Sourdough](https://sourdough.apps.wayne.edu/) start new apps. Booster is for the apps you already have. ![Booster logo: a syringe that is also a rocket, gold flame below](https://booster.apps.wayne.edu/brand/booster-avatar-512.png) - **Altitude**: PHP 8.5 - **Stage**: 6 of 6 - **Gate**: target-image tests Dose by dose ### A stamp for each hop that passes. These are Booster's plans for the example apps in `examples/`, written up like a vaccination card. Pick an app and press Run the plan. Form B-01 · Version immunization record #### examples/laravel-blade - **App type**: laravel-blade - **Course**: PHP 8.2 → 8.5 · Laravel 10 → 13 · PHPUnit 10 → 13 - **Policy**: waynestate/creed 0.1.6 - **Next booster due**: 17 Mar 2028 1. 01 `phpunit-10-to-11` Move PHPUnit 10 tests to PHPUnit 11. PHP 8.2 · Laravel 10 booster-target:8.2 Gate passed PHP 8.2 28 Oct 2026 2. 02 `laravel-10-to-11` Move a Laravel 10 app to Laravel 11. PHP 8.2 · Laravel 11 booster-target:8.2 Gate passed PHP 8.2 28 Oct 2026 3. 03 `laravel-11-to-12` Move a Laravel 11 app to Laravel 12. PHP 8.2 · Laravel 12 booster-target:8.2 Gate passed PHP 8.2 28 Oct 2026 4. 04 `php-82-to-83` Move PHP 8.2 code to PHP 8.3. PHP 8.3 · Laravel 12 booster-target:8.3 Gate passed PHP 8.3 28 Oct 2026 5. 05 `phpunit-11-to-12` Move PHPUnit 11 tests to PHPUnit 12. PHP 8.3 · Laravel 12 booster-target:8.3 Gate passed PHP 8.3 28 Oct 2026 6. 06 `laravel-12-to-13` Move a Laravel 12 app to Laravel 13. PHP 8.3 · Laravel 13 booster-target:8.3 Gate passed PHP 8.3 28 Oct 2026 7. 07 `php-83-to-84` Move PHP 8.3 code to PHP 8.4. PHP 8.4 · Laravel 13 booster-target:8.4 Gate passed PHP 8.4 28 Oct 2026 8. 08 `phpunit-12-to-13` Move PHPUnit 12 tests to PHPUnit 13. PHP 8.4 · Laravel 13 booster-target:8.4 Gate passed PHP 8.4 28 Oct 2026 9. 09 `php-84-to-85` Move PHP 8.4 code to PHP 8.5. PHP 8.5 · Laravel 13 booster-target:8.5 Gate passed PHP 8.5 28 Oct 2026 RESULT: PASS Review with `git diff`, run your own checks, then commit. Booster never commits or pushes. The overlap ### Never on a pair Laravel doesn't support. Booster mixes the PHP and Laravel hops so the app is on a supported pair after every hop. Laravel 10 runs on PHP 8.1 to 8.3, 11 on 8.2 to 8.4, 12 on 8.2 to 8.5, 13 on 8.3 or newer. If you ask for a pair that can't exist, Booster refuses before anything runs. The target also has to be one CREED's policy knows. A plan can pass through PHP 8.1 and even stop there, with a warning that says so: ``` $ bin/booster plan --path=examples/raw-php --to-php=8.1 BOOSTER plan for examples/raw-php app type: raw-php PHP: 7.4 -> 8.1 ... Warning: PHP 8.1 is past security support since 2025-12-31. Use it only as a stop on the way up. exit 0 ``` supported pair not a pair Laravel supports the Laravel and PHP hops for Laravel 10 on PHP 8.2 Grid of Laravel versions 10 to 13 against PHP 8.1 to 8.5. The path starts at Laravel 10 on PHP 8.2, moves to Laravel 11 and then 12 on PHP 8.2, PHP 8.3, Laravel 13, then PHP 8.4 and PHP 8.5. Inside one dose ### Four steps before the next stage fires. 1. T−4 ### Dry run and review Rector previews the hop. If it reports errors, the run stops before anything is written. So does a hop over the review threshold of 200 files: an interactive run shows the per-rule summary and asks first, and `--max-changes` sets a hard limit instead. 2. T−3 ### Rector for real Rector runs with only that hop's rules, from Booster's own `vendor/`. That's how an app on PHP 7.4 gets rewritten by a Rector running on 8.3 or newer. 3. T−2 ### Raise the constraint The `php` or `laravel/framework` constraint in `composer.json` goes up, and `config.platform.php` moves with it. 4. T−1 ### The gate Before the first hop your tests run once on the app's current PHP as the baseline; a red one stops the run unless you pass `--allow-failing-baseline`. An app with no tests of its own gets a built-in syntax check (php -l on every PHP file outside vendor/ and node_modules/) instead, and the report says so: it proves the code loads on the new PHP, not that it works. After each hop they run inside `booster-target:`, built from `docker/Dockerfile` on the PHP the hop moves to. On Laravel and PHPUnit hops the gate refreshes `composer.lock` for the package the hop moved, so the lock lands on the new major version; if Packagist's security advisories block those releases, whether the install fails outright or Composer quietly locks a branch version where the hop asked for a tag, the gate writes a scoped temporary ignore, retries once, and tells you what it did. The plan's last step then fails until that entry is out of `composer.json`, so the upgrade never finishes with the unblock still in place. After the last hop PROCTOR checks the upgraded app against CREED's policy, blocking only on findings the upgrade introduced. If a red baseline names a missing Mix or Vite manifest, build the front-end assets first. A failure stops the run and leaves the changes for you to read. `git checkout .` undoes them. Not every hop is Rector. `node-pin-24` pins Node 26 in `package.json` and writes `.nvmrc`; `mix-to-vite` converts a simple `webpack.mix.js` and moves the build to Vite. The Vue 2 to 3 move is staged: `vue-26-to-27`, `vue-27-to-compat` and `vue-compat-codemod`, with the `vue-2-to-3` playbook standing in for all three. Rector doesn't read Vue, so a person works those steps. The plan also flags PHP 7.4 to 8.0 as the hop that needs the most review. The commands ### The whole kit. There's also `scan`, which reports what will break on a given PHP (`--removals` for removed functions, `--phpstan` for static analysis) and always exits 0. Exit codes: **0** pass · **1** the baseline failed, or a hop was capped, errored or failed its gate · **2** bad usage or a target the policy does not know. Want to see it run? [Real output](https://booster.apps.wayne.edu/proof.html) shows the plan and the dry run for each example app, captured from the real tool. **detectWhich of the five stack shapes, and which versions** $ bin/booster detect --path=examples/vue-spa path examples/vue-spa app_type raw-php-vue composer_json yes php 8.0 laravel unknown phpunit unknown vue 2 node unknown mix_file no wayne_cas no wayneui_1 no boost no helmet no Nothing was changed. This is what Booster read from composer.json and package.json; "php" is the lowest PHP version composer.json allows, not what your server runs (if it is wrong, add --from-php=X.Y to plan). Keys: composer_json = composer.json found, mix_file = webpack.mix.js found, wayne_cas = the cit-web-infra/wayne-cas package, wayneui_1 = the cit-web-infra/wayne-ui package, boost = the laravel/boost package, helmet = the waynestate/helmet package. Next: bin/booster plan --path=examples/vue-spa **planThe ordered hops, the warnings and the scans. Changes nothing.** $ bin/booster plan --path=examples/raw-php --today=2026-10-29 BOOSTER plan for examples/raw-php app type: raw-php PHP: 7.4 -> 8.5 policy: waynestate/creed 0.1.6 (today 2026-10-29) 1. php-74-to-80 Move PHP 7.4 code to PHP 8.0. After this step your tests run in Docker. Give this hop the most review time. Removed between PHP 7.4 and PHP 8.0: none found. Behavior changes to check: Loose comparison of a number with a non-numeric string is now false: 0 == "foo", 0 == "" and 42 == "42foo" flip from true to false; \< / \<= against negative numbers also flip for such strings check: Audit loose comparisons of request parameters and string DB columns Default error_reporting is now E_ALL (previously excluded E_NOTICE and E_DEPRECATED); notices and deprecations now surface in logs and, with display_errors on, in output keep old: error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED in php.ini check: Watch error logs and rendered pages for new notices after the hop 2. php-80-to-81 Move PHP 8.0 code to PHP 8.1. After this step your tests run in Docker. Removed between PHP 8.0 and PHP 8.1: none found. 3. php-81-to-82 Move PHP 8.1 code to PHP 8.2. After this step your tests run in Docker. Removed between PHP 8.1 and PHP 8.2: none found. 4. php-82-to-83 Move PHP 8.2 code to PHP 8.3. After this step your tests run in Docker. Removed between PHP 8.2 and PHP 8.3: none found. 5. php-83-to-84 Move PHP 8.3 code to PHP 8.4. After this step your tests run in Docker. Removed between PHP 8.3 and PHP 8.4: none found. 6. php-84-to-85 Move PHP 8.4 code to PHP 8.5. After this step your tests run in Docker. Removed between PHP 8.4 and PHP 8.5: none found. PHPCompatibility check for PHP 8.0: no findings. PHPStan on PHP 8.5: 3 findings (code quality hints, not upgrade blockers). src/Legacy.php:20 \[missingType.parameter\] Method Example\\Legacy::className() has parameter $object with no type specified. src/Legacy.php:22 \[return.type\] Method Example\\Legacy::className() should return string but returns class-string|false. src/Legacy.php:25 \[parameter.implicitlyNullable\] Deprecated in PHP 8.4: Parameter #1 $label (string) is implicitly nullable via default value null. Nothing was changed. The numbered list above is the order Booster would do things; "PHPCompatibility check" and "PHPStan" are advice for you to read, not errors. Booster does every numbered step above when you run it. Preview the edits: bin/booster run --path=examples/raw-php --dry-run (The real run needs Docker, and your app committed in git: bin/booster run --path=examples/raw-php) **runRuns the plan; --dry-run previews every hop** $ bin/booster run --path=examples/raw-php --to-php=8.2 --dry-run BOOSTER dry run (nothing was changed) Baseline: SKIPPED (no tests ran in this run). A real run first runs your tests as they are now (the baseline), then again after every step (the gate). 1. php-74-to-80: PREVIEWED (1 file) Largest risk hop: loose comparison of numbers and strings changed, many warnings became thrown errors, and arithmetic on arrays throws. Write or extend tests before applying this hop; only tests and review find behavior changes here. Behavior changes to check: Loose comparison of a number with a non-numeric string is now false: 0 == "foo", 0 == "" and 42 == "42foo" flip from true to false; \< / \<= against negative numbers also flip for such strings check: Audit loose comparisons of request parameters and string DB columns Default error_reporting is now E_ALL (previously excluded E_NOTICE and E_DEPRECATED); notices and deprecations now surface in logs and, with display_errors on, in output keep old: error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED in php.ini check: Watch error logs and rendered pages for new notices after the hop --- src/Legacy.php --- Original +++ New @@ -4,22 +4,18 @@ class Legacy { - /\*\* @var string \*/ - private $name; - - public function \_\_construct(string $name) + public function \_\_construct(private string $name) { - $this->name = $name; } public function mentionsAdmin(string $text): bool { - return strpos($text, 'admin') !== false; + return str_contains($text, 'admin'); } public function className($object): string { - return get_class($object); + return $object::class; } public function label(string $label = null): string 2. php-80-to-81: PREVIEWED (0 files) No edits needed for this step (dry run). mysqli now throws on errors by default; PDO on MySQL returns native int and float types; htmlspecialchars() now escapes single quotes. Passing null to a non-nullable built-in parameter is deprecated: check the log after the run. 3. php-81-to-82: PREVIEWED (0 files) No edits needed for this step (dry run). Unique files across hops: 1. Later hops were previewed against the original tree, so the same file can appear in several hops. RESULT: PASS (previewed, no tests ran) Dry run: these changes were previewed, none were applied. Nothing was changed. RESULT: PASS means the preview worked; no tests ran. Next, if the edits look right: bin/booster run --path=examples/raw-php (It needs Docker and a clean git folder. Commit or stash first.) **hopsEvery hop Booster knows** $ bin/booster hops php-55-to-56 Move PHP 5.5 code to PHP 5.6. php-56-to-70 Move PHP 5.6 code to PHP 7.0. php-70-to-71 Move PHP 7.0 code to PHP 7.1. php-71-to-72 Move PHP 7.1 code to PHP 7.2. php-72-to-73 Move PHP 7.2 code to PHP 7.3. php-73-to-74 Move PHP 7.3 code to PHP 7.4. php-74-to-80 Move PHP 7.4 code to PHP 8.0. php-80-to-81 Move PHP 8.0 code to PHP 8.1. php-81-to-82 Move PHP 8.1 code to PHP 8.2. php-82-to-83 Move PHP 8.2 code to PHP 8.3. php-83-to-84 Move PHP 8.3 code to PHP 8.4. php-84-to-85 Move PHP 8.4 code to PHP 8.5. laravel-5.8-to-6 Move a Laravel 5.8 app to Laravel 6. laravel-6-to-7 Move a Laravel 6 app to Laravel 7. laravel-7-to-8 Move a Laravel 7 app to Laravel 8. laravel-8-to-9 Move a Laravel 8 app to Laravel 9. laravel-9-to-10 Move a Laravel 9 app to Laravel 10. laravel-10-to-11 Move a Laravel 10 app to Laravel 11. laravel-11-to-12 Move a Laravel 11 app to Laravel 12. laravel-12-to-13 Move a Laravel 12 app to Laravel 13. phpunit-9-to-10 Move PHPUnit 9 tests to PHPUnit 10. phpunit-10-to-11 Move PHPUnit 10 tests to PHPUnit 11. phpunit-11-to-12 Move PHPUnit 11 tests to PHPUnit 12. phpunit-12-to-13 Move PHPUnit 12 tests to PHPUnit 13. phpunit-to-9-manual Move PHPUnit to ^9 by hand: PHPUnit below 9 does not run on PHP 8, so later gates cannot run until it moves. \[playbook\] cas-to-sesame Swap the wayne-cas package for SESAME using its compatibility classes. wayneui-1-to-2 Move WayneUI 1.x (Bootstrap 3) to 2.x (wsu- classes): rewrites the one-to-one items and lists the rest. node-pin-24 Pin the app to Node 26: set engines.node in package.json and write .nvmrc. mix-to-vite Move a simple Laravel Mix front end to Vite (simple webpack.mix.js files only). tailwind-3-to-4 Move a Tailwind CSS 3 front end to Tailwind CSS 4 with the official upgrade tool. vue-26-to-27 Move a Vue 2.6 front end to Vue 2.7, the last Vue 2 release, which carries the Vue 3 reactivity API. vue-27-to-compat Move a Vue 2.7 front end to Vue 3 through the @vue/compat build. vue-compat-codemod Run the vue-upgrade-tool codemod over the front end and keep every manual item it lists. vue-2-to-3 Move a Vue 2 front end to Vue 3 (playbook only; Rector does not read Vue). \[playbook\] **initWrites the coding-tab playbooks to .agents/skills** $ bin/booster init --path=../my-app Wrote 35 playbooks (one for each step Booster knows, plus a general one), 35 changed. $ bin/booster init --path=../my-app --check Playbooks are current. # exits 1 when a playbook is stale, so CI notices Get the shot ### Install, then plan. Run Booster from inside the app you want to upgrade: `--path` defaults to `.`, so `booster plan` needs no flags. Booster needs PHP 8.3 or newer on the machine; the app it upgrades does not. **Single file (PHAR)** ``` mkdir -p ~/bin curl -L -o ~/bin/booster.phar https://github.com/waynestate/booster/releases/latest/download/booster.phar alias booster='php ~/bin/booster.phar' # put this line in ~/.bashrc or ~/.zshrc to keep it cd /path/to/your-app booster detect booster plan booster run --dry-run booster run ``` One shot, properly administered: the phar carries everything, so the machine needs PHP 8.3 or newer and nothing else. `detect` reads the app and prints its type and versions. `plan` lists the steps. `run --dry-run` previews the edits and changes nothing. `run` edits files and makes no commit. A real `run` also needs git, a clean git folder (commit or stash first) and a Docker daemon for its test gate. **Linux, macOS and WSL** ``` sudo apt install php-cli php-xml php-mbstring php-zip php-curl unzip git composer git clone https://github.com/waynestate/booster.git cd booster && composer install bin/booster --version bin/booster plan --path=/path/to/your-app ``` Ubuntu splits the XML extension out of `php-cli`, and Composer's dev dependencies need it: when Composer says 'ext-dom is missing', run that line. On macOS: `brew install php composer git`. Ubuntu 22.04 ships PHP 8.1, too old for Booster: add the ondrej/php PPA or use the Docker route below. Never clone Booster inside an app, and `--path` takes a full path, not `~`. In WSL, keep Booster and the app in your Linux home folder, not under `/mnt/c` (a Windows drive is slow from Linux); a Windows folder is `/mnt/c/Users//...`, and `run` needs Docker Desktop's WSL integration switched on. **Windows PowerShell** ``` # PowerShell, with PHP and Composer installed git clone https://github.com/waynestate/booster.git cd booster; composer install php bin\booster --version php bin\booster plan --path=..\my-app ``` This route assumes PHP 8.3 or newer and Composer are already on PATH. For a real `run`, git and a Docker daemon are needed too. **Docker, no PHP** ``` # inside the booster clone; the path after the second -v is your app docker run --rm -v "${PWD}:/booster" -w /booster composer:2 composer install --no-interaction docker run --rm -v "${PWD}:/booster" -v "/path/to/app:/app" -w /booster php:8.4-cli php bin/booster plan --path=/app ``` This route can run `detect`, `plan`, `scan` and `run --dry-run`. A real `run` starts Docker and uses git from inside Booster, which the `php:8.4-cli` image lacks, so for a real run use the single-file or clone route (in WSL on Windows). When Booster itself runs in a container, pass `--skip-gate` to `run` and run the tests yourself. The one-shot form, for a forced image upgrade `bin/booster run --path=../my-app --goal=php-74-to-84` On Packagist It's `waynestate/booster` on Packagist. Give it its own folder, not your app's `vendor/`, because it needs PHP 8.3 even when your app doesn't. Clone it as above, or run `composer create-project waynestate/booster booster ^1.0`. Build the phar You can also build the single file from any commit with `scripts/build-phar.ps1` (PowerShell) or `scripts/build-phar.sh` (Linux, macOS). Each writes `build/booster.phar` and verifies it by printing Booster's version. Both need Docker; neither needs PHP or Composer on the host. The docs The guides and the command reference live at [booster.apps.wayne.edu/docs/](https://booster.apps.wayne.edu/docs/). They are built from `docs/` by `bin/docs-site.php`, and the built pages are committed with the release. For your coding agent ### Standing orders. Six prompts for whatever coding agent you use: one to get started, then one for each app shape. Copy one, or open it in OpenCode, Claude Code, Codex or ChatGPT. Each starts with a plan that changes nothing, and none of them lets the agent commit for you. #### Get started with Booster Clones Booster next to your app, runs `detect` and `plan`, and reports back. Nothing in your app changes. A prompt for your coding agent. Copy it as it stands: ```text You are a coding agent planning a PHP and Laravel upgrade with Booster, for the app in this folder, at Wayne State University. Booster is the upgrade tool of WELD, the Wayne State application stack: it plans the hops from the versions the app runs today to ones CREED's version policy still supports, and runs them one at a time on Rector, with the app's own tests as the gate on each hop. Read first: - https://booster.apps.wayne.edu/llms.txt, the index of Booster's pages, each one as Markdown; llms-full.txt beside it holds the README with every command and option. Fetch what you need; do not work from memory. - ADVISOR, the stack's MCP server, if it is connected: upgrade_path for this app, policy_lookup for PHP and Laravel, and read_project_docs for booster. If it is not connected, say so and work from the pages. How to add it: https://advisor.apps.wayne.edu/llms.txt - This app's composer.json, and package.json if there is one. Task: 1. Clone Booster into its own folder beside the app with `git clone https://github.com/waynestate/booster.git`, then run `composer install` in it. Booster needs PHP 8.3 or newer; the app does not. If composer names a missing ext-, install php-xml, php-mbstring, php-zip and php-curl first (sudo apt install ...). No PHP on the machine: use the Docker lines from the README. 2. Run bin/booster detect --path=../my-app (on Windows PowerShell, php bin\booster with ..\my-app) and say which of the five stack shapes the app is and which versions it runs. 3. Run bin/booster plan --path=../my-app and give me the ordered hops, the warnings, what will break on each hop's target PHP, and the PHPStan findings for the final target. 4. Say which hops are Rector, which edit manifests and which are playbooks a person works through. Constraints: - Plan only. Do not run bin/booster run, not even with --dry-run, until I say so. - Booster never commits, pushes or opens a merge request, and neither do you: a person reads the diff and merges it. - Ask me before you guess: a version the manifest does not state, a target the policy refuses, or anything that touches CI or a deploy. Output: the shape and versions, the plan as a numbered list, the warnings and scan findings, then your questions. ``` #### Or write the order for your app's shape #### Upgrade a raw PHP app Plans the PHP hops, scans for what will break and does a dry run. The real run waits until you say so. A prompt for your coding agent. Copy it as it stands: ```text You are a coding agent upgrading a raw PHP app (no framework, no Vue) at Wayne State University with Booster, the upgrade tool of WELD, the Wayne State application stack. Read first: - https://booster.apps.wayne.edu/llms.txt, then the README in llms-full.txt beside it: Quick start, What a hop does and The baseline. - ADVISOR, the stack's MCP server, if it is connected: upgrade_path for this app, and list_stack_rules with permutation raw-php. If it is not connected, say so and work from the pages. Task: 1. With Booster cloned beside the app, run bin/booster detect --path=../my-app and confirm the shape is raw-php. 2. Run bin/booster plan --path=../my-app. Expect PHP hops only, one minor at a time. 3. Run bin/booster scan --path=../my-app --php=X.Y for the first hop's target and list what will break; add --removals for removed functions and --phpstan for static analysis. 4. Run bin/booster run --path=../my-app --dry-run and summarize the diffs per hop. 5. Only when I say so: bin/booster run --path=../my-app. It needs a clean git working copy and a Docker daemon for the gate, which runs `composer test` (or vendor/bin/phpunit) in the target PHP image. Constraints: - One hop at a time; a red gate stops the run and leaves the changes for me to read (`git checkout .` undoes them). Never commit or push. - If a hop is over the review threshold, show me its per-rule summary before applying it. Do not use --max-changes=0 or --skip-gate without asking me. - Ask me before you guess: a red baseline, a capped hop, or a target the policy refuses. Output: the plan, the scan findings, the dry-run summary per hop, then your questions. ``` #### Upgrade a raw PHP app with a Vue front end Plans the PHP hops and the Node pin, and stages the Vue 2 to 3 move: the compat build, then the codemod, with a person doing the Vue work. A prompt for your coding agent. Copy it as it stands: ```text You are a coding agent upgrading a raw PHP app with a Vue single-page front end at Wayne State University with Booster, the upgrade tool of WELD, the Wayne State application stack. Read first: - https://booster.apps.wayne.edu/llms.txt, then the README in llms-full.txt beside it: Quick start and Hops Booster knows. - ADVISOR, the stack's MCP server, if it is connected: upgrade_path for this app, policy_lookup for Node and Vue, and list_stack_rules with permutation raw-php-vue. If it is not connected, say so and work from the pages. - package.json and its lock file. Task: 1. With Booster cloned beside the app, run bin/booster detect --path=../my-app and confirm the shape is raw-php-vue, with the Vue and Node versions. 2. Run bin/booster plan --path=../my-app and sort the hops: PHP hops (Rector), node-pin-24 (pins Node 26 in package.json and writes .nvmrc), and the staged Vue hops if the app is on Vue 2: vue-26-to-27 when it is below 2.7, then vue-27-to-compat and vue-compat-codemod. 3. Run bin/booster run --path=../my-app --dry-run and summarize the diffs. 4. For the Vue hops, list the playbook steps for this app: the compat build, then the codemod, keeping every manual item the codemod lists. Rector does not read Vue, so Booster changes nothing there: a person does that work. Constraints: - Plan and dry run only, until I say so. Never commit or push. - Node hops run npm in a node:26 container, so they need a Docker daemon but no Node on the machine. - Ask me before you guess: a front end you cannot place, or a lock file that is missing. Output: the plan sorted into PHP, Node and Vue, the dry-run summary, the Vue playbook steps, then your questions. ``` #### Upgrade a Laravel app with Blade Laravel and PHP hops in the order that keeps a supported pair at every step, and SESAME if the app still signs in with wayne-cas. A prompt for your coding agent. Copy it as it stands: ```text You are a coding agent upgrading a Laravel app with Blade views at Wayne State University with Booster, the upgrade tool of WELD, the Wayne State application stack. Read first: - https://booster.apps.wayne.edu/llms.txt, then the README in llms-full.txt beside it: Quick start, Laravel hops and vendor, and The baseline. - ADVISOR, the stack's MCP server, if it is connected: upgrade_path for this app, policy_lookup for PHP and Laravel, and list_stack_rules with permutation laravel-blade. If it is not connected, say so and work from the pages. Task: 1. Run `composer install` in the app first: rector-laravel needs Laravel's classes, and a Laravel hop without vendor is only a partial preview. 2. With Booster cloned beside the app, run bin/booster plan --path=../my-app and explain the order: it keeps the app on a PHP and Laravel pair Laravel supports at every step. 3. If composer.json requires wayne-cas, run the plan again with --sesame and say what the cas-to-sesame hop renames. Only the two-factor sign-in stays manual; list that for a person. 4. Run bin/booster run --path=../my-app --dry-run and summarize the diffs per hop. Constraints: - Plan and dry run only, until I say so. Never commit or push. - Laravel and PHPUnit hops refresh composer.lock inside the gate (composer update -W for the package the hop moved), and a Composer-phase failure means the tests never ran. PHP hops keep a plain install: if a committed lock goes stale, say which packages need composer update. - Ask me before you guess: a red baseline, a capped hop, or a package that blocks a Laravel major. Output: the plan with the reason for its order, the SESAME notes if any, the dry-run summary, then your questions. ``` #### Upgrade a Laravel app with Vue islands The Laravel and PHP hops, the Node pin, and Mix to Vite when the plan lands on Laravel 9 or newer. A prompt for your coding agent. Copy it as it stands: ```text You are a coding agent upgrading a Laravel app with Blade views and Vue islands at Wayne State University with Booster, the upgrade tool of WELD, the Wayne State application stack. Read first: - https://booster.apps.wayne.edu/llms.txt, then the README in llms-full.txt beside it: Hops Booster knows (node-pin-24, mix-to-vite, vue-26-to-27, vue-27-to-compat, vue-compat-codemod) and Laravel hops and vendor. - ADVISOR, the stack's MCP server, if it is connected: upgrade_path for this app, policy_lookup for Laravel and Node, and list_stack_rules with permutation laravel-blade-vue. If it is not connected, say so and work from the pages. - package.json, its lock file, and webpack.mix.js if there is one. Task: 1. Run `composer install` in the app, then, with Booster cloned beside it, bin/booster plan --path=../my-app. 2. If the plan has mix-to-vite, say whether this webpack.mix.js is the simple shape Booster converts (mix.js, .vue(), .sass(), .postCss(), .version(), and if (mix.inProduction()) { mix.version(); }) or which call it will refuse by name. A Vue 2 front end takes the staged Vue hops first: vue-26-to-27 when it is below 2.7, then vue-27-to-compat and vue-compat-codemod. 3. Run bin/booster run --path=../my-app --dry-run and summarize the diffs, Blade layouts included. 4. List any withoutMix() calls for me to rename to withoutVite() by hand. Constraints: - Plan and dry run only, until I say so. Never commit or push. - The mix-to-vite gate runs npm ci and npm run build in a node:26 container before the tests, so it needs a Docker daemon. - Ask me before you guess: a Mix config Booster refuses, or an asset no Mix entry covers. Output: the plan, the Mix to Vite verdict, the dry-run summary, the manual renames, then your questions. ``` #### Upgrade a Laravel app with Inertia The Laravel and PHP hops and the Node pin, with the Inertia checks a person still does by hand. A prompt for your coding agent. Copy it as it stands: ```text You are a coding agent upgrading a Laravel app with Inertia and Vue at Wayne State University with Booster, the upgrade tool of WELD, the Wayne State application stack. Read first: - https://booster.apps.wayne.edu/llms.txt, then the README in llms-full.txt beside it: Quick start, Laravel hops and vendor, and Hops Booster knows. - ADVISOR, the stack's MCP server, if it is connected: upgrade_path for this app, policy_lookup for Laravel, Node and Vue, and list_stack_rules with permutation laravel-inertia-vue. If it is not connected, say so and work from the pages. - composer.json, package.json and its lock file. Task: 1. Run `composer install` in the app, then, with Booster cloned beside it, bin/booster detect --path=../my-app and bin/booster plan --path=../my-app. 2. Sort the hops: Laravel and PHP (Rector), node-pin-24, and the staged Vue hops if the app is on Vue 2 (playbooks, not code changes): vue-26-to-27 when it is below 2.7, then vue-27-to-compat and vue-compat-codemod. 3. For each Laravel hop, list what to check by hand on the Inertia side: Booster has no Inertia checks per Laravel hop yet. 4. Run bin/booster run --path=../my-app --dry-run and summarize the diffs per hop. Constraints: - Plan and dry run only, until I say so. Never commit or push. - A Laravel hop without vendor is only a partial preview; say so if you see one marked incomplete. - Ask me before you guess: an inertiajs package that blocks a Laravel major, or a red baseline. Output: the plan sorted by kind, the Inertia checks per Laravel hop, the dry-run summary, then your questions. ``` The agent reads the same pages you do: [llms.txt](https://booster.apps.wayne.edu/llms.txt) lists them, [llms-full.txt](https://booster.apps.wayne.edu/llms-full.txt) holds them all in one file, and this page has a [Markdown twin](https://booster.apps.wayne.edu/index.md). Nothing here phones home. The buttons only copy text or open the tool you picked. Contraindications ### What Booster will not do. Side effects are limited to the diff in your working copy. Read it, then merge it yourself. - ### Commit, push or open a merge request It edits the working copy and prints a report. A person reads the diff and merges it. - ### Start on a dirty tree `run` refuses a working copy that is not under git or has uncommitted changes, so the upgrade diff stands alone. Pass `--allow-dirty` if you must. - ### Aim at an unsupported version An end-of-life target is refused; a security-only one comes with a warning. The dates come from CREED's `policy.json`, never from Booster's code. - ### Resolve more than the hop moves Inside the gate it runs a scoped `composer update -W` for the package a Laravel or PHPUnit hop moved, so the lock follows the constraint. It never resolves anything else; you decide when to update the rest. - ### Pass quietly An unreadable policy, broken JSON, missing Docker or a Rector error stops the run with exit 1 or 2. - ### Trust your file names Subprocesses take argument arrays, and control characters are stripped from everything printed. It reads no secrets and makes no network calls of its own. The care team ### Booster's place in WELD. WELD is the Wayne State application stack. Booster is its upgrade layer, and it works for all five shapes: raw PHP, raw PHP with a Vue SPA, Laravel with Blade, Blade with Vue islands, and Inertia with Vue. - [WayneUI](https://wayneui.apps.wayne.edu/)The interface layer under everything, this page included. - [CREED](https://creed.apps.wayne.edu/)Writes the version policy Booster reads from the waynestate/creed package. - [PROCTOR](https://proctor.apps.wayne.edu/)Measures an app against that policy; the gate runs it after the last hop, and `--proctor-command` overrides the check. - [Preflight](https://preflight.apps.wayne.edu/)The CI templates Booster's own checks run on. - [SESAME](https://sesame.apps.wayne.edu/)Sign-in; `--sesame` moves wayne-cas apps to it. - [HELMET](https://helmet.apps.wayne.edu/)Security headers and session hardening. - [Pinstripe](https://pinstripe.apps.wayne.edu/)The coding standard. Booster's PHPStan advisory scan runs against its raw preset. - [Sourdough](https://sourdough.apps.wayne.edu/)Starts new raw PHP apps on current versions. - [WELD](https://weld.apps.wayne.edu/)The Laravel and Vue starter, with the stack already installed. - BoosterMoves apps forward, one dose at a time.You are here - [Squire](https://squire.apps.wayne.edu/)Carries the release out the door. - [Rollcall](https://rollcall.apps.wayne.edu/)Every app answers “present”, with its PHP version. - [ADVISOR](https://advisor.apps.wayne.edu/)An MCP server your agent can ask about the stack. - [Drawbridge](https://drawbridge.apps.wayne.edu/)The sign-in gateway, lowered on request. Version 1.8 ### Next The six example apps prove the plumbing end to end, and the gate now ends with PROCTOR checking the result against CREED's policy. What stays open, from [OPEN_QUESTIONS.md](https://github.com/waynestate/booster/blob/main/OPEN_QUESTIONS.md): - Not yet Custom WSU Rector rules beyond the CAS rename, and Rector pilot numbers on real apps - Not yet A cas-to-sesame run against a real app; Duo two-factor stays manual work - Decided No `booster mcp`: ADVISOR is the stack's read-only MCP server - Decided No merge-request automation on app code; a person reads the diff and merges it - Open The shared MySQL 5.6 database floor (question 8); the hop notes tell the reviewer to check it --- Source: https://booster.apps.wayne.edu/docs/checks.html ## Checks[#](https://booster.apps.wayne.edu/docs/checks.html#checks) This page is for maintainers: how Booster is checked, and where its proof lives. Next: [Next](https://booster.apps.wayne.edu/docs/next.html) for what is not built yet. Maintainers: how to build, test and release this project, and where its data comes from, is in [MAINTAINING.md](https://github.com/waynestate/booster/blob/main/MAINTAINING.md). Checks: `composer ci` (the same command runs on GitLab and GitHub). Development happens on Wayne State's GitLab; this repository is a release mirror. Changes land there first. ``` composer ci ``` runs `composer validate --strict`, PHPUnit, Pint, PHPStan (Pinstripe's raw preset, inlined; `waynestate/pinstripe` ships with Booster and supplies the preset the advisory scan uses), `composer audit`, the hop data check and the agent-ready check. Nothing needs PHP on the host: ``` docker run --rm -v "${PWD}:/app" -w /app composer:2.10.3 sh -c "composer install --no-interaction && composer ci" ``` `contract/expectations.json` says what Booster must detect, plan and change for each example app in `examples/`; `tests/ContractTest.php` enforces it with the real Rector, so a Rector release that changes a hop shows up in CI. ### Proof[#](https://booster.apps.wayne.edu/docs/checks.html#proof) Proof runs against real Wayne State applications are on a private site, [proof.apps.wayne.edu/booster](https://proof.apps.wayne.edu/booster/), behind a Wayne State employee sign-in. The [Real output page](https://booster.apps.wayne.edu/proof.html) shows Booster's own plan and dry-run output on each example app, and `composer ci` keeps it current. Proven how: the first full run with the gate on took a Laravel 12 app from PHP 8.2 to 8.4 in two hops, each gated by the app's 197-test suite, and it is written up in [research/booster-e2e-gate.md](https://github.com/waynestate/booster/blob/main/research/booster-e2e-gate.md). ### Status[#](https://booster.apps.wayne.edu/docs/checks.html#status) See [CHANGELOG.md](https://github.com/waynestate/booster/blob/main/CHANGELOG.md) for what changed, [OPEN_QUESTIONS.md](https://github.com/waynestate/booster/blob/main/OPEN_QUESTIONS.md) for what stays open, and [BEST_PRACTICES.md](https://github.com/waynestate/booster/blob/main/BEST_PRACTICES.md) for the rules Booster follows. --- Source: https://booster.apps.wayne.edu/docs/guides/baseline.html ## The baseline[#](https://booster.apps.wayne.edu/docs/guides/baseline.html#the-baseline) This page explains the baseline `run` takes before the first hop. Next: [A smoke test for WSU apps](https://booster.apps.wayne.edu/docs/guides/smoke-test.html). Before the first hop, `run` runs the app's own tests (or the built-in `php -l` syntax check when the app has no tests) on the PHP the app is on now, in the same `booster-target:` image the gate uses. The point is to tell "Booster broke it" from "it was already broken": a red baseline means the suite was red before any change, so a later red gate is not proof of a bad hop. The baseline is recorded in the report (`passed`, `failed` or `skipped`, with the exit code and the last 20 lines of output). - A red baseline stops the run before the first hop, with a non-zero exit. `--allow-failing-baseline` lets it go on; each later gate failure is then labeled "the baseline was already failing" in the text and JSON output. - `--skip-gate` and `--dry-run` skip the baseline (reported as `skipped`), so a dry run never starts Docker. - A Mix app's `public/mix-manifest.json` is a build artifact, not committed source: a fresh clone has no manifest, so the suite can be red before any hop. Build the assets before the baseline (the app's usual `npm ci && npm run build`), or pass `--allow-failing-baseline` knowing why. - When the app has no tests of its own, the baseline and every gate run a built-in check instead: every PHP file outside `vendor/` and `node_modules/` must still parse on the target PHP (`php -l`). That proves the code loads on the new PHP, not that it works: click through the app before you ship. Pass `--test-command` to run something else, or add a smoke test ([A smoke test for WSU apps](https://booster.apps.wayne.edu/docs/guides/smoke-test.html)) to get the real gate back. --- Source: https://booster.apps.wayne.edu/docs/guides/behaviour-changes.html ## Behavior changes[#](https://booster.apps.wayne.edu/docs/guides/behaviour-changes.html#behavior-changes) This page explains the behavior-change blocks `plan` and `run` print, one per PHP and Laravel hop. Next: [The baseline](https://booster.apps.wayne.edu/docs/guides/baseline.html). Some changes between versions do not raise an error: a function that still exists now means something else, a config key that was optional is required, a default that flipped. Rector rewrites what it knows; the silent changes need a human eye. So `plan` and `run` carry a curated list of them, one block per PHP and Laravel hop, plus the package rows on the last step, where the lock lands. A booster should know its own payload. ``` Behavior changes to check: Config key needed (config/app.php) check: Add the key. Minimum PHP raised check: Check the runtime. ``` Each entry says what changed (`label`), what keeps the old behavior when there is anything (`keep old:`), what to verify (`check:`), and where the app touches it: `file:line` hits for code, the file for config and package rows, no location for rows that apply everywhere. It is advisory, like the scans: it never changes the exit code, and nothing prints when nothing matches. In JSON output the entries land under `behaviour_changes`, per step, with the fields `id`, `label`, `keep_old`, `check`, `source`, `hits` and `total`. An entry may also carry an optional `parity` object: one observable thing, how to read it before the hop, what it must equal after, and how to measure it. When a matched entry has one, the block gains a `Parity checks` section under the same step, one `[ ]` row per entry, so you can write down the before value now and compare it after the hop. The field is optional; entries without it print exactly as before. ```json { "parity": { "what": "How long a flashed toast stays open", "before": "The timer value the app sets today (SWEET_ALERT_TIMER in .env or 'timer' in config/sweetalert.php)", "after": "SWEET_ALERT_TOAST_AUTO_CLOSE (or 'toast.auto_close') must equal that value", "how": "Fire a toast and time it with a stopwatch" } } ``` The list lives in `data/behaviour-changes.json`, bundled with the phar. An entry names the hop it applies to (`php-80-to-81`, `laravel-10-to-11`, or `package:vendor/name@2` for a floor a package raises), how to find it (`detect`: a `function`, `method`, `class`, `property`, string `token`, blade snippet, config `file` and `key`, composer `package`, or `always`), and a `source` link that confirms the change: ```json { "id": "laravel-11-config-key", "hop": "laravel-10-to-11", "detect": { "kind": "config", "file": "config/app.php", "key": "cipher" }, "label": "Config key needed", "keep_old": null, "check": "Add the key.", "source": "https://laravel.com/docs/11.x/upgrading" } ``` Only confirmed changes ship, each with its source link, and at most three `always` rows per release, so the block stays a checklist rather than a pamphlet. To add an entry, append it to the data file and let the loader's errors tell you what is missing. --- Source: https://booster.apps.wayne.edu/docs/guides/citadel.html ## Deploying on Citadel[#](https://booster.apps.wayne.edu/docs/guides/citadel.html#deploying-on-citadel) This page covers the deployment script Booster writes for a real run. Next: [Checks](https://booster.apps.wayne.edu/docs/checks.html). When a real (not dry) run changes `composer.lock` and the app has a `deployments/` directory, Booster writes one deployment script, `deployments/0000_composer_install_`: ```sh #!/bin/sh # Added by Booster: this upgrade changed composer.lock. Citadel runs each deployment script once, in name order, # so a new name makes existing deployments install the new vendor/ before anything else runs. set -e composer install --no-dev --no-interaction --optimize-autoloader php artisan migrate --force ``` Citadel runs every `deployments/` script exactly once, in name order, and the leading `0000` puts this one first. So when the app next deploys, every existing deployment installs the upgraded `vendor/` before any other script runs. The `php artisan migrate --force` line appears only for apps that have an `artisan` file; raw PHP apps get the `composer install` alone. Commit the script with the rest of the upgrade. To drop it again, delete the named file. --- Source: https://booster.apps.wayne.edu/docs/guides/dependency-check.html ## The dependency check[#](https://booster.apps.wayne.edu/docs/guides/dependency-check.html#the-dependency-check) This page explains the Packagist check Booster runs before the hops print. Next: [Behavior changes](https://booster.apps.wayne.edu/docs/guides/behaviour-changes.html). Before the hops print, `plan` (and `run`) asks Packagist about each direct third-party dependency in the app's `composer.json`: what is its newest stable release, and can that release still be installed at each hop's destination? Only plain `require` and `require-dev` entries count; PHP, extensions (`ext-*`, `lib-*`), Laravel itself (`laravel/framework`, its `illuminate/*` components) and PHPUnit are Booster's own business, so they are skipped. The answer is advisory, like the scans: a package that cannot follow the plan does not drop the hop. It adds a warning that names the package, the hop it blocks, and what to do about it: ``` acme/legacy-auth 2.3.0 supports Laravel up to 10 and PHP up to 8.3, so it blocks laravel-10-to-11. Replace or upgrade it first. ``` The warning names only the dimensions the blocked hop fails on: a Laravel ceiling alone, a PHP ceiling alone, or both together. A ceiling is printed as the newest version Booster plans that the constraint allows (a PHP constraint capping at 7.x reads "7.4", not "7"), and a constraint with no upper bound has no ceiling to name, so the warning names its floor instead (`needs PHP 8.2 or newer`). A package whose newest release requires several `illuminate/*` components is judged by the loosest of those constraints, the one that allows the most Laravel versions. Abandoned packages get their own warning (`acme/old is abandoned (use acme/new instead).`). Packages the check cannot reach are reported as `Not checked: (not on Packagist or unreachable).` or `(VCS repository).`, and the plan goes on. In JSON output every package lands under `package_checks`, with the fields `package`, `newest`, `blocks`, `abandoned` and `checked`. The check fetches from `repo.packagist.org` and from any composer-type repository the app declares in `composer.json`; VCS repositories are never fetched. `--no-package-check` skips the whole check, for offline use. --- Source: https://booster.apps.wayne.edu/docs/guides/hops.html ## What a hop does[#](https://booster.apps.wayne.edu/docs/guides/hops.html#what-a-hop-does) This page walks through one hop step by step. The full list of hops is in [the reference](https://booster.apps.wayne.edu/docs/reference/hops.html). On PHP hops a pre-pass runs first: PHP 4 era `$a = &new Foo();` becomes a plain assignment, because PHP 7 and newer refuse to compile the old spelling and Rector would never get a parse tree. The report lists the rewrites per file under `Pre-pass fixes:`. 1. Rector runs as a dry run. If it reports errors, the run stops before anything is written. So does a hop that would change more than 200 files, unless an interactive run confirms it after showing the per-rule summary; an explicit `--max-changes` is a hard limit that never asks. An empty Rector report usually means the process ran out of memory, and the message points at `--memory-limit`. 2. Rector runs for real, with only that hop's rules. A rule that would change what the code does is never applied: the PHP 8.0 set's switch-to-match rewrite is skipped, because `match` compares strictly and would stop a `case 1:` from matching the string `"1"` a query string carries. Every run keeps Rector's and PHPStan's caches in its own temp directory, removed at exit, so one run never reads another's. 3. `composer.json` is edited: the `php` or `laravel/framework` constraint is raised, and `config.platform.php` moves with it when the app already pins it. Booster never creates that key. 4. The gate runs the app's tests inside `booster-target:-`, a container built from `docker/Dockerfile` on the PHP the hop moves to. The stamp is a short hash of the recipe, so an edited `docker/Dockerfile` rebuilds the image instead of reusing a stale one. The image carries the extension set Citadel's apps run with: intl, ldap, soap, zip, gd, pdo_mysql, mysqli, bcmath, pcntl, exif, redis, imagick and oci8. The first build downloads Oracle Instant Client for oci8, so it takes a few minutes; later hops reuse the image. Pass `--image` to run the gate in your own image instead (below). A red gate stops the run and leaves the changes in place so you can read them (`git checkout .` undoes them). To check the image itself, `scripts/check-target-image.sh` (or its Windows twin `check-target-image.ps1`) builds it for each PHP version and verifies its extension list; it needs Docker and a few minutes per version, so it is not part of `composer ci`. 5. On Laravel and PHPUnit hops the gate refreshes the lock before the tests: `composer update -W` for the package the hop moved, so `composer.lock` lands on the new major version next to `composer.json`. Every other hop, and the baseline, run a plain `composer install`. The two phases report separately: a Composer failure names the Composer phase and Composer's own output, because the tests never ran. Composer 2.10 refuses to load tagged releases Packagist has flagged with a security advisory, and hops that move onto Laravel 9 or 10 most often meet that block. The refusal has two faces. The loud one fails the install phase and names the advisories. The quiet one happens when the app allows dev stability: Composer does not fail at all, it locks the branch instead, `9.x-dev` where the hop asks for `^9.0`. Booster reads the refreshed lock back and treats a branch version behind a tagged range like the block. Either way it writes a temporary entry to `config.policy.advisories.ignore` in `composer.json`: scoped to the package and the constraint Composer refused, kept visible to `composer audit`, and carrying a reason PROCTOR can find and report until the app removes it. The gate then retries once. If the retry still fails, the run stops with the unblock and the failure both in the report, and the entry stays in `composer.json`. The unblock is temporary by name, and Booster enforces that. When a gated step passes, Booster re-reads `config.policy.advisories.ignore` and reports every entry whose reason carries its own prefix (`Booster temporary unblock`; older runs wrote `BOOSTER temporary unblock`, which it still recognizes). A step that is not the plan's last one applies with a warning; the plan's last step fails until the entry is out of `composer.json`, so the final target never ships with the unblock still in place. Removing the entry is the app's job once it is off the blocked versions. The `--image` value names the image the baseline and every gate run in, and it takes three shapes: - One name, used for the baseline and every gate: `--image myapp/gate:8.4`. - A template whose `{php}` is replaced with the PHP version of that baseline or gate: `--image registry/php-apache:{php}` gates the 8.1 hop on `registry/php-apache:8.1` and the 8.2 hop on `registry/php-apache:8.2`. - A map of PHP version to image, for a fleet whose images do not follow one pattern: `--image 8.0=legacy/php:7.4,8.1=registry/php-apache:8.1`. A map without the version a baseline or hop lands on stops the run before the first hop and names the fix; a template never misses. Booster never builds a `--image` image: it must exist locally or be pullable, and each resolved image is pulled at most once per run. On an arm64 Mac, Citadel's published images such as `citadel/php-apache:8.5r3` are amd64 only: pass an arm64 build via `--image`, or set `DOCKER_DEFAULT_PLATFORM=linux/amd64` and let Docker emulate. `booster-target` itself builds natively on arm64. And the old-PHP images (5.5 through 8.0) point apt at archive.debian.org, so they get no security updates: they exist to run the app's tests on the way up, not to serve traffic. `run` refuses to start on a working copy that is not under git or has uncommitted changes, so the upgrade diff stands alone. `--dry-run` has no such limit. A dry run previews every hop against the original tree, so the same file can appear in several hops. The report ends with the number of unique files and a reminder that nothing was applied. A Laravel hop whose app has no `vendor/` cannot see the framework's classes, so its preview is marked incomplete and the report says to run `composer install` first. A run prints one line per hop to standard error while it works: the hop's position in the plan, its id, its status, the number of files it changed and the seconds it took, and a final line totals the hops run, the stops and the whole run. They go to standard error so the report on standard output is untouched and `--format json` stays valid JSON. Three hops edit manifests instead of code. `node-pin-24` sets `engines.node` in `package.json` and writes `.nvmrc`, then the app's usual gate applies. `mix-to-vite` converts a simple `webpack.mix.js` to Vite (see [the hop list](https://booster.apps.wayne.edu/docs/reference/hops.html)) and asks for a build gate, which runs `npm ci` and `npm run build` in the `node:26` container before the app's tests. `tailwind-3-to-4` runs the official `@tailwindcss/upgrade` tool in the Node container, then pins `tailwindcss` to `^4.1`, adds the matching Vite or PostCSS plugin, and removes `autoprefixer` and `postcss-import`, which Tailwind 4 folds in; plugins listed in `tailwind.config.js` and `@apply` inside Vue SFC `