CRM Leads Server is the back end of a small customer-relationship tool. A separate single-page app logs in through it, lists leads by date range, records follow-ups, and manages client accounts. Leads arrive from CallRail, a call-tracking service, through a webhook.
It runs on Laravel 7.20 and PHP 7.2, both unsupported for years. PHP 7.2 stopped receiving security fixes in November 2020 and Laravel 7 in March 2021. The dependency lock file dates from mid-2020 and three of its packages are abandoned by their authors.
The good news is that the application is small and conventional. The automated rewrite tools found no PHP-language breakage in the application code; the work is in the framework and package layers, which are well documented. We recommend moving to PHP 8.4 and Laravel 13 in four stages behind a safety net of behavior-pinning tests, with the old runtime kept alive until the final cutover.
.env file, and the separate front-end that consumes this API.| Area | What it does |
|---|---|
| Authentication | Session login for the SPA through Sanctum; registration, password reset and email verification scaffolding from laravel/ui |
| Users | Admin creates and updates client accounts, each with a profile holding a company name and CallRail account id; a welcome email is sent |
| Leads | Lists a client's leads by preset date range; bulk-imports leads from a CallRail payload; updates a lead's status |
| Follow-ups | Records dated follow-up notes against a lead |
| Webhook | POST /api/webhook/callrail/{profile} stores an incoming call as a lead |
Direct dependencies only; the 89 transitive packages follow these.
| Package | Locked | Latest | Status | What happens |
|---|---|---|---|---|
| laravel/framework | 7.20 | 13.38 | active | Six major releases to cross |
| laravel/sanctum | 2.4 | 4.3 | active | Config and migration changes; expires_at column added |
| laravel/ui | 2.1 | 4.6 | active | Provides Auth::routes(); must move to 4.x |
| laravel/tinker | 2.4 | 3.0 | active | Version bump only |
| guzzlehttp/guzzle | 6.5 | 8.3 | active | Laravel 8+ requires Guzzle 7 or newer |
| fideloper/proxy | 4.4 | 4.4 | unnecessary | Replaced by the framework's own TrustProxies since Laravel 9 |
| fruitcake/laravel-cors | 1.0 | 3.0 | abandoned | CORS is built into Laravel 9+; package removed |
| swiftmailer transitive | 6.2 | 6.3 | abandoned | Laravel 9 switched to Symfony Mailer automatically |
| fzaninotto/faker dev | 1.9 | 1.9 | abandoned | Replaced by fakerphp/faker |
| facade/ignition dev | 2.3 | 2.17 | superseded | Replaced by spatie/laravel-ignition |
| nunomaduro/collision dev | 4.2 | 8.9 | active | Version bump |
| phpunit/phpunit dev | 8.5 | 13.4 | active | Laravel 13 wants PHPUnit 12; test base classes change |
Rector, run with the full PHP 8.3 rule set, proposed changes in 21 files, all of them cosmetic: closure return types, arrow functions, rand() to random_int(). No removed functions, no removed extensions, no dynamic-property or implicit-nullable problems in the application code. PHPStan at level 1 reports 28 issues, of which 24 are missing return statements in empty scaffold methods and facade aliases it cannot see; the real ones are in section 6.
From the official upgrade guides, limited to what this codebase actually touches.
laravel/legacy-factories. The seeds folder is renamed. Routes use string controller names; keep the namespace property on the route provider or convert to class references. Guzzle 7.HEADER_X_FORWARDED_ALL constant used in the TrustProxies middleware; switch to the framework's version with explicit headers. Remove the proxy and CORS packages; the framework's own CORS middleware replaces the latter. Swift Mailer becomes Symfony Mailer; both Mailables use only subject and markdown, which are unchanged. Re-publish the mail templates.diffIn* results, which this code does not use.PreventRequestForgery and gains request-origin checks using the Sec-Fetch-Site header; the webhook exclusions must be carried over and the SPA's cross-site requests verified against the new check. Session serialization defaults to JSON and cache gains an allow-list for serialized classes; both are config decisions at cutover. PHPUnit 12.Laravel Mix 5 and Vue 2.5. The SPA that users see is a separate application, so this scaffold only compiles one example component nobody renders. Options: move to Vite, the current Laravel default, or delete the unused scaffold. Vue 2 reached end of life in December 2023.
Automated coverage. Rector handles the cosmetic PHP changes. Everything in the framework list is small but manual: about 12 files change, none by more than a few lines. The effort is in verification, not typing.
| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| 1 | No tests exist; a regression would reach the SPA unnoticed | High | High | Stage 1 writes behavior-pinning tests before any change |
| 2 | The SPA depends on exact JSON shapes (controllers return raw models) | High | High | Tests assert the JSON returned by every endpoint today |
| 3 | Laravel 13's origin check blocks the SPA's cross-site requests | Medium | High | Verify stateful domains and the new header check on staging before cutover |
| 4 | CallRail webhook payload shape is undocumented in the repo | Medium | Medium | Capture real payloads from production logs; replay in tests |
| 5 | IIS hosting may not offer PHP 8.4 | Medium | Medium | Confirm with the host in week 1; Linux hosting is the fallback |
| 6 | Sanctum 4 token table needs a new column | Certain | Low | Published migration adds expires_at; run at cutover |
| 7 | API routes run under the web middleware group, which is unusual but deliberate for cookie auth | Certain | Low | Preserve as is; document it |
Found while reading the code. Not included in the migration price; the owner decides what to do with each.
Items 1, 3 and 4 are security issues and we recommend fixing them in the same engagement; they add about one day.
Today: two example tests, no application behavior covered.
Before changing a line, we write characterization tests that pin what the application does now, including its quirks, and run them against the current runtime in a Docker container with PHP 7.4 and the locked dependencies. Then every stage must keep them green on the new runtime.
About 25 tests. They ship with the project and become the regression suite the next developer inherits.
| Stage | Scope | Deliverable | Effort |
|---|---|---|---|
| 1 | Safety net | Docker setup for the old runtime; characterization tests green on PHP 7.4 / Laravel 7 | 2 days |
| 2 | Runtime and dependencies | PHP 8.4, Laravel 7 to 10, Guzzle 7, factories, proxies, CORS, mailer, PHPUnit 10; tests green | 2 days |
| 3 | Current framework | Laravel 10 to 13, Sanctum 4, PHPUnit 12, CSRF middleware rename, config sync; tests green | 2 days |
| 4 | Front-end build | Vite replaces Mix, or the unused scaffold is removed | 0.5 day |
| 5 | Cutover | Runbook, env diff, staging deploy, SPA smoke test, production, two weeks of watch | 1.5 days |
Each stage lands as a pull request with passing tests. Production keeps running on the old runtime until stage 5.
| Package | Price | Includes |
|---|---|---|
| Assessment (this document) | $1,500 | Credited against the migration if ordered within 30 days |
| Migration, stages 1 to 5 | $6,500 fixed | Everything in section 8, the test suite, the runbook, 30 days of post-cutover fixes |
| Security fixes, items 1, 3 and 4 | $900 | Set-password flow, signed webhook, sanitized error responses, with tests |
| Keep-current retainer | $500 / month | Framework and PHP updates within 60 days of release, dependency audits, security patches, test suite maintained |
Excluded: changes to the separate SPA, hosting migration, new features.
Produced by Rector 2 with the PHP 8.3 set and the Laravel level set up to 12, and PHPStan 2 at level 1, against a copy of the repository with its locked dependencies installed.
| Tool | Result |
|---|---|
| Rector, PHP 8.3 rules | 21 files, 20 rule applications: 13 closure void return types, 4 arrow functions, 2 rand() to random_int(), 1 string class name to constant |
| Rector, Laravel rules to 12 | 13 cosmetic changes, all closure return types; no framework API rewrites needed |
| PHPStan, level 1 | 28 findings: 10 missing returns in empty scaffold methods, 7 facade aliases unresolved, 7 Eloquent static calls unresolved without Larastan, 3 $this in console routes, 1 class-name case mismatch |