Modernization assessment

Attendize

Event ticketing platform, version 2.8.0 · public open-source project assessed as a sample engagement 22 October 2026, revised 23 October Prepared by keepPhpUp
PHP
7.3
8.4
Framework
Laravel 6.18
Laravel 13
Security fixes
ended 2021–22
to Dec 2028
Migration
$24,000
fixed price · about six weeks

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.

1. Summary

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.

2. What we looked at

AreaWhat it does
Accounts and organisersSign-up, login, password reset, email confirmation; an account owns organisers, organisers own events
EventsCreate and customise events, ticket types, access codes, surveys, embedded widgets, promotion
CheckoutReserve 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 attendeesManage, message, cancel and refund orders; import attendees from a spreadsheet; export attendees, orders and survey answers; print and resend PDF tickets
Check-inSearch and QR-code check-in at the door
APIA small token-authenticated read API for events and attendees

3. Dependency health

Direct production dependencies with a decision attached. Status comes from Packagist on the assessment date.

PackageLockedLatestStatusWhat happens
laravel/framework6.1813.38activeSeven major releases to cross
laravelcollective/html6.06.4abandoned583 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/wkhtml2pdfdev-master2.1 (2013)unmaintainedGenerates 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-money2.02.0abandonedUsed for refund arithmetic. Replace with brick/money
fideloper/proxy4.24.4unnecessaryFramework TrustProxies since Laravel 9; the header constant it relies on is gone in Symfony 6
doctrine/dbal2.104.5decisionLaravel 11 dropped it; keep at 3.x for the historic column-change migrations or squash the schema
guzzlehttp/guzzle6.58.3activeLaravel 8 requires 7
intervention/image2.54.4two majors11 call sites in image uploads; the 3.x and 4.x API differs from 2.x
maatwebsite/excel3.14.0activeNew major in 2026 requiring PHP 8.3; the export and import classes use stable concerns and should carry over
omnipay / omnipay-stripe / stripe-php3.0 / 3.1 / 7.223.2 / 3.2 / 22.0verifyOmnipay 3 stays; stripe-php 7 to 22 is many majors and must be tested against Stripe test mode
mcamara/laravel-localization1.42.4activeRoute-prefix middleware; 2.x needs config changes
graham-campbell/markdown11.116.1activeCommonMark 2 underneath; the markdown helpers are unchanged
mews/purifier3.13.4activeThe clean() helper is unchanged
milon/barcode6.013.5activeQR codes on tickets; API stable
dompdf/dompdf0.83.1activePulled in but PDFs go through wkhtml2pdf today
laravel/socialite4.35.31unusedNot referenced in application code; can be removed
laracasts/utilities, vinelab/http, php-http/*variouslow maintenanceCheck for live call sites; remove what is unused
fzaninotto/faker dev1.91.9abandonedReplaced by fakerphp/faker
phpspec/phpspec dev6.18.3misusedImported by three production files by mistake, see section 6
phpunit/phpunit dev8.512 for Laravel 13activeTest base class and assertion changes

4. What breaks on the target

PHP 7.3 to 8.4

Automated results are in the appendix. From reading the code, the PHP-level work is small and specific:

Laravel 6 to 13, release by release

Limited to what this codebase touches, from the official upgrade guides.

Front-end build

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.

5. Risk register

#RiskLikelihoodImpactMitigation
1Payment flow regresses: Omnipay, Stripe Payment Intents, several stripe-php majors, a 3-D Secure redirectHighCriticalStage 1 scripts the full checkout against Stripe test mode with the dummy and Stripe gateways; it runs on every stage
2PDF tickets stop generating when the bundled wkhtmltopdf binary meets a new OS or ARM hostHighHighReplace the engine in stage 5 and snapshot-test the ticket PDF
363 views break when the form helper is replacedCertainHighRewrite view by view with a rendered-HTML snapshot test per screen
4Objects stored in the session break under JSON serialization or Eloquent changesHighHighRefactor the checkout session to ids and scalars before Laravel 13
5Carbon 3 changes the semantics used by the checkout countdownMediumMediumCharacterization test on the countdown; pin behaviour
6ISO date serialization changes API responses and exportsMediumMediumSerialization override on the base model; export snapshot tests
7The web installer and upgrader write the environment file and run migrations from a browser requestCertainMediumKeep behind the installed-file check; verify it cannot be re-triggered after cutover
8Queued jobs serialize models and a service object; old payloads in the queue at cutover fail on the new codeMediumLowDrain queues before cutover; version the job classes
9Eight deprecated tests represent lost coverage of signup, login, events and organisersCertainMediumRestore the ones that still describe current behaviour in stage 1
10No 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 checkoutCertainMediumStage 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
11Laravel 9 changes the top-level message of every 422, which the checkout's AJAX code may displayHighMediumStage 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
12The Laravel 13 origin check cannot be verified by tests, because the framework disables that middleware under testCertainHighStage 5 verifies the embedded iframe against a running server over HTTP, and adds a test that fails if that coverage is ever silently disabled

6. Findings beyond the upgrade

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.

  1. A misspelled catch block swallows nothing.payments
    The order-creation method catches \Exeption, so any gateway exception during payment surfaces as a server error with no logging and no friendly message.
  2. The order-completion rollback never fires.data
    Order completion wraps its database transaction in a catch for 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.
  3. PDF tickets are served from a public folder.security
    Tickets live under the public directory named by order reference, which is five random characters plus the day and month. Anyone who can guess or enumerate a reference can download a ticket. Move tickets to private storage with signed links.
  4. The tenant scope builds SQL by string concatenation.security
    The value is the authenticated account id today, but it should be a query binding.
  5. A controller calls exit()bug
    when JavaScript is disabled, ending the request outside the framework; return a response instead.
  6. Translated yes/no strings are interpolated into SQLhygiene
    in the attendee and order exports. Safe today, fragile tomorrow.
  7. Test card data and a dummy gateway ship in configpayments
    and are selectable when test payments are enabled; ensure production pins it off.
  8. API tokens are a plain columndesign
    compared by the legacy token guard; acceptable, but a candidate for Sanctum once on Laravel 13.
  9. Hygiene.hygiene
    A deprecated tests folder, three build systems for the front-end, and an unused Socialite dependency.

7. Test coverage and verification plan

Today: 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.

8. Staged plan

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.

StageScopeDeliverableEffort
1Safety netDocker baseline on the shipped PHP 7.4 image; characterization tests; Stripe test-mode script; fixes for findings 1 to 31.5 weeks
2Laravel 6 to 8, still on PHP 7.4Exception handler type hint, date serialization override, legacy factories, seeders, Bootstrap pagination, Guzzle 7; tests green0.5 week
3Laravel 9, on PHP 8.1PHP 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 green1 week
4Laravel 12, on PHP 8.4, passing through 10 and 11PHP 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 test1.5 weeks
5Laravel 13Checkout 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 alone0.5 week
6PDF enginewkhtml2pdf replaced; ticket template adjusted; snapshot tests0.5 week
7CutoverRunbook, Docker image on PHP 8.4, staging with a copy of production data, queue drain, go-live, two weeks of watch0.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.

9. Estimate and options

PackagePriceIncludes
Assessment (this document)$3,500Credited against the migration if ordered within 30 days
Migration, stages 1 to 7$24,000 fixedEverything in section 8, about 120 tests, the runbook, the Docker image, 30 days of post-cutover fixes
Secure ticket storage, finding 3$1,800Private disk, signed download links, migration of existing files
Front-end modernizationquoted separatelyReplace Bower, Grunt and Vue 1 with Vite and current libraries, keeping the design
Keep-current retainer$1,200 / monthFramework 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.

Appendix A. Laravel 13 dependency probe

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.

PackageVerdict
laravelcollective/htmlBlocker. Its newest release, 6.4.1, tops out at illuminate/view ^10.0
fideloper/proxyAlso uninstallable, and removed during the migration regardless
The other 26All resolve on Laravel 13
spatie/laravel-htmlThe 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.

Appendix B. 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 rules86 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 1278 files: the same closure types plus 11 method renames ($request->get() to $request->input()) and 1 property rename. No structural rewrites.
PHPStan, level 1784 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.
Dependencies38 direct packages checked against Packagist: 3 abandoned, 1 unmaintained since 2013, 14 with at least one major version to cross.
Sources: PHP supported-versions schedule; Laravel upgrade guides 7.x through 13.x; Packagist metadata retrieved 22 October 2026; the Attendize repository at its development branch. Versions and prices are correct on the date above.