Revision note, 23 October 2026. Section 8's stage sequence is corrected. The original plan raised PHP ahead of the framework, which is not possible, and it rested on Laravel 10, which cannot be installed at all. Three items are added to sections 4 and 5, and the dependency position is now verified rather than read from Packagist. The scope, the total of about six weeks and the price are unchanged; what changes is the order of the work and what we verify at the end of it.
Attendize is a self-hosted event ticketing platform: organisers publish events, sell tickets with Stripe or offline payment, email PDF tickets, check attendees in, and export attendee and order lists. It is a real, mid-sized product with about 43,500 lines of PHP and 210 views, and roughly 4,300 stars on GitHub.
It runs on Laravel 6.18 and PHP 7.3, shipped in a Docker image built on PHP 7.4. Laravel 6 left security support in September 2022 and PHP 7.3 in December 2021. The last commit is from January 2023. The dependency lock holds 97 production packages, several of them abandoned, including the HTML form helper that 63 views depend on and the PDF engine that generates every ticket.
The application code is conventional Laravel and the automated tools find little PHP-language breakage. The work is in three places: six framework releases, a dozen third-party packages that need replacing or crossing major versions, and the checkout flow, which stores objects in the session and talks to Stripe through an abstraction that must be tested end to end. We recommend PHP 8.4 and Laravel 13, delivered in six stages over about six weeks behind characterization tests, with the current runtime kept in production until cutover.
/install that writes the environment file and runs migrations.| Area | What it does |
|---|---|
| Accounts and organisers | Sign-up, login, password reset, email confirmation; an account owns organisers, organisers own events |
| Events | Create and customise events, ticket types, access codes, surveys, embedded widgets, promotion |
| Checkout | Reserve tickets for a timed window, collect attendee details and survey answers, pay by Stripe, Stripe Payment Intents, a dummy gateway, or offline; affiliate tracking |
| Orders and attendees | Manage, message, cancel and refund orders; import attendees from a spreadsheet; export attendees, orders and survey answers; print and resend PDF tickets |
| Check-in | Search and QR-code check-in at the door |
| API | A small token-authenticated read API for events and attendees |
Direct production dependencies with a decision attached. Status comes from Packagist on the assessment date.
| Package | Locked | Latest | Status | What happens |
|---|---|---|---|---|
| laravel/framework | 6.18 | 13.38 | active | Seven major releases to cross |
| laravelcollective/html | 6.0 | 6.4 | abandoned | 583 Form:: and 63 Html:: calls across 63 of the 210 views, plus five custom macros, must move to plain Blade or spatie/laravel-html. Only 25 distinct helper methods are used, so the surface to reimplement is small even though the call sites are many. Verified: no version of this package installs alongside Laravel 13, and spatie/laravel-html does. The critical path of the whole migration. |
| nitmedia/wkhtml2pdf | dev-master | 2.1 (2013) | unmaintained | Generates every PDF ticket from a bundled wkhtmltopdf binary. Replace with knp-snappy on a system wkhtmltopdf, or move to Dompdf 3 and adjust the ticket template |
| superbalist/php-money | 2.0 | 2.0 | abandoned | Used for refund arithmetic. Replace with brick/money |
| fideloper/proxy | 4.2 | 4.4 | unnecessary | Framework TrustProxies since Laravel 9; the header constant it relies on is gone in Symfony 6 |
| doctrine/dbal | 2.10 | 4.5 | decision | Laravel 11 dropped it; keep at 3.x for the historic column-change migrations or squash the schema |
| guzzlehttp/guzzle | 6.5 | 8.3 | active | Laravel 8 requires 7 |
| intervention/image | 2.5 | 4.4 | two majors | 11 call sites in image uploads; the 3.x and 4.x API differs from 2.x |
| maatwebsite/excel | 3.1 | 4.0 | active | New major in 2026 requiring PHP 8.3; the export and import classes use stable concerns and should carry over |
| omnipay / omnipay-stripe / stripe-php | 3.0 / 3.1 / 7.22 | 3.2 / 3.2 / 22.0 | verify | Omnipay 3 stays; stripe-php 7 to 22 is many majors and must be tested against Stripe test mode |
| mcamara/laravel-localization | 1.4 | 2.4 | active | Route-prefix middleware; 2.x needs config changes |
| graham-campbell/markdown | 11.1 | 16.1 | active | CommonMark 2 underneath; the markdown helpers are unchanged |
| mews/purifier | 3.1 | 3.4 | active | The clean() helper is unchanged |
| milon/barcode | 6.0 | 13.5 | active | QR codes on tickets; API stable |
| dompdf/dompdf | 0.8 | 3.1 | active | Pulled in but PDFs go through wkhtml2pdf today |
| laravel/socialite | 4.3 | 5.31 | unused | Not referenced in application code; can be removed |
| laracasts/utilities, vinelab/http, php-http/* | various | low maintenance | Check for live call sites; remove what is unused | |
| fzaninotto/faker dev | 1.9 | 1.9 | abandoned | Replaced by fakerphp/faker |
| phpspec/phpspec dev | 6.1 | 8.3 | misused | Imported by three production files by mistake, see section 6 |
| phpunit/phpunit dev | 8.5 | 12 for Laravel 13 | active | Test base class and assertion changes |
Automated results are in the appendix. From reading the code, the PHP-level work is small and specific:
number_format() and string functions receiving null from nullable columns raise deprecations on 8.1 and later; the money helper and several views need null guards.Str::Random work but should be normalised.$_ENV lookups depend on the server's variables order; use env().Limited to what this codebase touches, from the official upgrade guides.
Throwable. Eloquent JSON serialization switches to ISO-8601 dates, which changes the read API's output; override serializeDate on the base model to preserve the old format. Markdown mail templates are re-published. MAIL_DRIVER becomes MAIL_MAILER, and the installer writes the old key, so the installer changes too.Mail::send closures and six Mailables use only standard methods and should carry over. The wording of every validation failure changes: the top-level message of a 422 becomes a summary of the first error instead of the fixed string, while the per-field errors object stays the same. The checkout runs on AJAX responses, so whether the browser code reads that field has to be checked; if it does, the old wording is restored with a short override and the decision recorded.$dates property is deprecated in favour of casts; ten models use it and three override getDates(). No release of Laravel 10 can be installed. Composer blocks all of 10.0 to 10.50, because every one carries security advisories with no patched release in that line, so this version is passed through rather than rested on.$dates is gone: convert to casts. The framework no longer requires Doctrine DBAL. Carbon 3: diffInSeconds now returns a float and can be negative, and the checkout countdown uses it. This is also where the form helper stops working, so the view rewrite lands here at the latest. No release of Laravel 11 can be installed either, for the same reason as 10. The earliest installable Laravel is 12.69.PreventRequestForgery with origin checks, which matters for the embedded checkout iframe used on third-party sites. Session serialization defaults to JSON in the new config, but the checkout stores Eloquent models and the account's payment gateway object in the session, so either the session config stays on PHP serialization or the checkout is refactored to store ids. We recommend the refactor. PHPUnit 12. On the origin check, one thing is worth stating plainly because it changes how this stage is verified: Laravel switches the request-forgery middleware off entirely while tests run, so no test suite can prove the embedded checkout iframe still works. We verify it by driving a running server over HTTP the way a browser does, from a third-party origin. A related change goes in the handover note: Laravel 13 accepts a same-origin form post with no CSRF token at all, where 6 to 12 refused it.Bower, Grunt, LESS, Vue 1.0 and Bootstrap 3 date from 2015 and 2016. Bower is retired and the pinned packages have known vulnerabilities, but the compiled assets are committed and the application runs without rebuilding them. We propose leaving the browser code as is during the migration and quoting a front-end modernization separately.
Automated coverage. Rector handles the cosmetic PHP changes and a share of the framework deprecations. The view rewrite away from the form helper, the PDF engine replacement, the session refactor and the Stripe verification are manual. About 90 files change by hand, most of them views.
| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| 1 | Payment flow regresses: Omnipay, Stripe Payment Intents, several stripe-php majors, a 3-D Secure redirect | High | Critical | Stage 1 scripts the full checkout against Stripe test mode with the dummy and Stripe gateways; it runs on every stage |
| 2 | PDF tickets stop generating when the bundled wkhtmltopdf binary meets a new OS or ARM host | High | High | Replace the engine in stage 5 and snapshot-test the ticket PDF |
| 3 | 63 views break when the form helper is replaced | Certain | High | Rewrite view by view with a rendered-HTML snapshot test per screen |
| 4 | Objects stored in the session break under JSON serialization or Eloquent changes | High | High | Refactor the checkout session to ids and scalars before Laravel 13 |
| 5 | Carbon 3 changes the semantics used by the checkout countdown | Medium | Medium | Characterization test on the countdown; pin behaviour |
| 6 | ISO date serialization changes API responses and exports | Medium | Medium | Serialization override on the base model; export snapshot tests |
| 7 | The web installer and upgrader write the environment file and run migrations from a browser request | Certain | Medium | Keep behind the installed-file check; verify it cannot be re-triggered after cutover |
| 8 | Queued jobs serialize models and a service object; old payloads in the queue at cutover fail on the new code | Medium | Low | Drain queues before cutover; version the job classes |
| 9 | Eight deprecated tests represent lost coverage of signup, login, events and organisers | Certain | Medium | Restore the ones that still describe current behaviour in stage 1 |
| 10 | No installable resting point between Laravel 6 and 12.69: every release of 10 and 11 is advisory-blocked, so a commit pinned to either cannot be installed from a clean checkout | Certain | Medium | Stage 4 applies the 10, 11 and 12 upgrade guides in one dependency jump and lands on 12.69 or later. No stage is left on an uninstallable release |
| 11 | Laravel 9 changes the top-level message of every 422, which the checkout's AJAX code may display | High | Medium | Stage 3 checks what the browser code reads; if it reads that field, the old wording is restored by an exception-handler override and the shim recorded for later removal |
| 12 | The Laravel 13 origin check cannot be verified by tests, because the framework disables that middleware under test | Certain | High | Stage 5 verifies the embedded iframe against a running server over HTTP, and adds a test that fails if that coverage is ever silently disabled |
Found while reading the code. Not part of the migration price; the owner decides. Items 1 to 3 touch money and we recommend fixing them during stage 1.
\Exeption, so any gateway exception during payment surfaces as a server error with no logging and no friendly message.Exception, but the file imports a test-tool class under that name. Real database or model errors are not caught, the transaction is left open, and a half-written order can remain. The same import appears in two other production files.exit()bugToday: three live test files covering refunds and a utility, eight disabled tests, and no automated coverage of checkout, ticket generation, imports, exports or check-in.
Before any change we write characterization tests that pin current behaviour and run them on the old runtime in Docker (PHP 7.4, the locked dependencies, MySQL 5.7):
Roughly 120 tests. They ship with the project.
Two rules shape this sequence, and both are corrections to how such plans are usually drawn. The runtime rises in step with the framework, never ahead of it, because each Laravel release caps the PHP version it accepts: Laravel 8 does not run on PHP 8.4. And no stage ends on a release that cannot be installed, which rules out resting on Laravel 10 or 11.
| Stage | Scope | Deliverable | Effort |
|---|---|---|---|
| 1 | Safety net | Docker baseline on the shipped PHP 7.4 image; characterization tests; Stripe test-mode script; fixes for findings 1 to 3 | 1.5 weeks |
| 2 | Laravel 6 to 8, still on PHP 7.4 | Exception handler type hint, date serialization override, legacy factories, seeders, Bootstrap pagination, Guzzle 7; tests green | 0.5 week |
| 3 | Laravel 9, on PHP 8.1 | PHP 8.1 image; dynamic properties and null-argument deprecations; framework TrustProxies and CORS, Flysystem 3, Symfony Mailer, Intervention Image 3; the 422 message decision; tests green | 1 week |
| 4 | Laravel 12, on PHP 8.4, passing through 10 and 11 | PHP 8.4 image. The 10, 11 and 12 upgrade guides applied in one dependency jump, because neither 10 nor 11 can be installed: dates to casts, DBAL decision, Carbon 3 and the checkout countdown, markdown and localization majors. The form helper replaced across 63 views, each with its rendered-HTML snapshot test | 1.5 weeks |
| 5 | Laravel 13 | Checkout session refactored to ids and scalars; CSRF middleware rename; PHPUnit 12; config sync. The origin check verified against a running server with the embedded iframe, not by tests alone | 0.5 week |
| 6 | PDF engine | wkhtml2pdf replaced; ticket template adjusted; snapshot tests | 0.5 week |
| 7 | Cutover | Runbook, Docker image on PHP 8.4, staging with a copy of production data, queue drain, go-live, two weeks of watch | 0.5 week |
Six weeks in total, unchanged from the first issue of this report. The work is the same; only its order is. Each stage is a pull request with the suite green. Production stays on the old runtime until stage 7.
| Package | Price | Includes |
|---|---|---|
| Assessment (this document) | $3,500 | Credited against the migration if ordered within 30 days |
| Migration, stages 1 to 7 | $24,000 fixed | Everything in section 8, about 120 tests, the runbook, the Docker image, 30 days of post-cutover fixes |
| Secure ticket storage, finding 3 | $1,800 | Private disk, signed download links, migration of existing files |
| Front-end modernization | quoted separately | Replace Bower, Grunt and Vue 1 with Vite and current libraries, keeping the design |
| Keep-current retainer | $1,200 / month | Framework and PHP updates within 60 days of release, dependency audits, security patches, test suite maintained |
Excluded: new features, hosting changes beyond the Docker image, the front-end rebuild.
Run 23 October 2026. Each of the 28 non-framework production dependencies was resolved on its own against php ^8.3 and laravel/framework ^13.0, at any version, with the resolver platform pinned to PHP 8.4.27 and Composer's security-advisory policy left on. That policy is now part of what "installable" means: it is what blocks every release of Laravel 10 and 11.
| Package | Verdict |
|---|---|
| laravelcollective/html | Blocker. Its newest release, 6.4.1, tops out at illuminate/view ^10.0 |
| fideloper/proxy | Also uninstallable, and removed during the migration regardless |
| The other 26 | All resolve on Laravel 13 |
| spatie/laravel-html | The proposed replacement for the form helper. Resolves on Laravel 13 |
Resolving is not the same as being safe. Two of the 26 carry caveats the resolver cannot see: nitmedia/wkhtml2pdf resolves only because it declares almost no constraints, and the real risk is its bundled 2013-era binary meeting a new OS or an ARM host (risk 2); superbalist/php-money resolves but is abandoned and needs ext-bcmath in the target image.
Measured scale of the critical path, counted from the repository rather than estimated: 583 Form:: and 63 Html:: call sites across 63 of 210 views, behind just 25 distinct helper methods and 5 custom macros. Form::label and Form::text alone account for 307 of the 646 call sites. The concentration matters more than the total: 25 methods is a small surface to reimplement once and then apply mechanically, which is what makes a fixed price on this item possible.
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 | 86 files, 92 rule applications: 77 closure void return types, 5 null-coalescing, 4 elvis ternaries, 3 short arrays, 2 explicit public, 1 pow() to **. All cosmetic. |
| Rector, Laravel rules to 12 | 78 files: the same closure types plus 11 method renames ($request->get() to $request->input()) and 1 property rename. No structural rewrites. |
| PHPStan, level 1 | 784 findings: 447 facade aliases the tool cannot resolve without Larastan, 166 Eloquent static calls for the same reason, 127 undeclared properties, 27 undefined variables worth a look, 6 new static in the base model, a handful of missing returns. |
| Dependencies | 38 direct packages checked against Packagist: 3 abandoned, 1 unmaintained since 2013, 14 with at least one major version to cross. |