Modernization assessment

CRM Leads Server

Lead-tracking CRM for medical practices · sample engagement, details anonymized 22 October 2026 Prepared by keepPhpUp
PHP
7.2
8.4
Framework
Laravel 7.20
Laravel 13
Security fixes
ended 2020–21
to Dec 2028
Migration
$6,500
fixed price · about 8 working days

1. Summary

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.

2. What we looked at

AreaWhat it does
AuthenticationSession login for the SPA through Sanctum; registration, password reset and email verification scaffolding from laravel/ui
UsersAdmin creates and updates client accounts, each with a profile holding a company name and CallRail account id; a welcome email is sent
LeadsLists a client's leads by preset date range; bulk-imports leads from a CallRail payload; updates a lead's status
Follow-upsRecords dated follow-up notes against a lead
WebhookPOST /api/webhook/callrail/{profile} stores an incoming call as a lead

3. Dependency health

Direct dependencies only; the 89 transitive packages follow these.

PackageLockedLatestStatusWhat happens
laravel/framework7.2013.38activeSix major releases to cross
laravel/sanctum2.44.3activeConfig and migration changes; expires_at column added
laravel/ui2.14.6activeProvides Auth::routes(); must move to 4.x
laravel/tinker2.43.0activeVersion bump only
guzzlehttp/guzzle6.58.3activeLaravel 8+ requires Guzzle 7 or newer
fideloper/proxy4.44.4unnecessaryReplaced by the framework's own TrustProxies since Laravel 9
fruitcake/laravel-cors1.03.0abandonedCORS is built into Laravel 9+; package removed
swiftmailer transitive6.26.3abandonedLaravel 9 switched to Symfony Mailer automatically
fzaninotto/faker dev1.91.9abandonedReplaced by fakerphp/faker
facade/ignition dev2.32.17supersededReplaced by spatie/laravel-ignition
nunomaduro/collision dev4.28.9activeVersion bump
phpunit/phpunit dev8.513.4activeLaravel 13 wants PHPUnit 12; test base classes change

4. What breaks on the target

PHP 7.2 to 8.4

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.

Laravel 7 to 13, release by release

From the official upgrade guides, limited to what this codebase actually touches.

Front-end build

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.

5. Risk register

#RiskLikelihoodImpactMitigation
1No tests exist; a regression would reach the SPA unnoticedHighHighStage 1 writes behavior-pinning tests before any change
2The SPA depends on exact JSON shapes (controllers return raw models)HighHighTests assert the JSON returned by every endpoint today
3Laravel 13's origin check blocks the SPA's cross-site requestsMediumHighVerify stateful domains and the new header check on staging before cutover
4CallRail webhook payload shape is undocumented in the repoMediumMediumCapture real payloads from production logs; replay in tests
5IIS hosting may not offer PHP 8.4MediumMediumConfirm with the host in week 1; Linux hosting is the fallback
6Sanctum 4 token table needs a new columnCertainLowPublished migration adds expires_at; run at cutover
7API routes run under the web middleware group, which is unusual but deliberate for cookie authCertainLowPreserve as is; document it

6. Findings beyond the upgrade

Found while reading the code. Not included in the migration price; the owner decides what to do with each.

  1. Plain-text passwords leave the system.security
    Creating a user sets a plain-password field on the record, returns it in the JSON response, and prints it in the welcome email. Fix: never return or email the password; send a set-password link instead.
  2. Lead status is random.data
    Both the bulk import and the webhook assign "New Patient" or "Existing Patient" with a coin flip. A commented-out line shows the intended source, the CallRail tag. Every status in the database today is random.
  3. Webhook is unauthenticated.security
    Anyone who knows a profile id can post leads to it. CallRail supports signed webhooks; verify the signature.
  4. Database errors are returned to clients in full,security
    exposing SQL and schema details in the error responses of three controllers.
  5. A webhook route in the web routes file fails at runtimebug
    because the request class is type-hinted without an import. It appears to be dead code superseded by the API route and should be deleted.
  6. Relationship bugs.bug
    The platform's leads relation uses the wrong foreign key, and the profile's user relation is declared in the wrong direction. Neither is called by current code.
  7. A personal email address is hard-codedhygiene
    in the listener that forwards webhook leads.
  8. Weak password rulesecurity
    on API-created users, minimum 5 characters, versus 8 on self-registration.
  9. Repository hygiene.hygiene
    IDE folder untracked, one uncommitted change, no compiled assets, no CI.

Items 1, 3 and 4 are security issues and we recommend fixing them in the same engagement; they add about one day.

7. Test coverage and verification plan

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.

8. Staged plan

StageScopeDeliverableEffort
1Safety netDocker setup for the old runtime; characterization tests green on PHP 7.4 / Laravel 72 days
2Runtime and dependenciesPHP 8.4, Laravel 7 to 10, Guzzle 7, factories, proxies, CORS, mailer, PHPUnit 10; tests green2 days
3Current frameworkLaravel 10 to 13, Sanctum 4, PHPUnit 12, CSRF middleware rename, config sync; tests green2 days
4Front-end buildVite replaces Mix, or the unused scaffold is removed0.5 day
5CutoverRunbook, env diff, staging deploy, SPA smoke test, production, two weeks of watch1.5 days

Each stage lands as a pull request with passing tests. Production keeps running on the old runtime until stage 5.

9. Estimate and options

PackagePriceIncludes
Assessment (this document)$1,500Credited against the migration if ordered within 30 days
Migration, stages 1 to 5$6,500 fixedEverything in section 8, the test suite, the runbook, 30 days of post-cutover fixes
Security fixes, items 1, 3 and 4$900Set-password flow, signed webhook, sanitized error responses, with tests
Keep-current retainer$500 / monthFramework 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.

Appendix. Static scan output

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.

ToolResult
Rector, PHP 8.3 rules21 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 1213 cosmetic changes, all closure return types; no framework API rewrites needed
PHPStan, level 128 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
Sources: PHP supported-versions schedule; Laravel upgrade guides 8.x through 13.x; Packagist metadata retrieved 22 October 2026. Versions and prices are correct on the date above.