When upgrading a Laravel app from 12 to 13, why is bumping laravel/framework not the whole upgrade, and what does comparing against the new skeleton add?
answer
- guide sorted by likelihood of impact
- the app's files came from the skeleton
- framework defaults fill omitted keys
- skeleton-only hardening needs copying
- 12.x...13.x comparison
basics
~20 sThe framework package changes framework code, but an app's config files, bootstrap files and composer.json were copied from the laravel/laravel skeleton and do not update themselves. The upgrade guide plus a 12.x-to-13.x skeleton diff shows which of those to change.
solid answer
~50 sLaravel's 13 upgrade guide lists each breaking change with a **likelihood of impact** (High: dependencies such as `laravel/framework ^13.0`, `laravel/tinker ^3.0`, `phpunit/phpunit ^12.0` or `pestphp/pest ^4.0`, the installer, the CSRF middleware rename; Medium: cache `serializable_classes`, MySQL/MariaDB `upsert` with empty `uniqueBy` now throwing). That covers framework code. The app's own files were copied from the skeleton when it was created, and the guide asks you to review the `laravel/laravel` 12.x...13.x comparison for changes it does not list. Some of those changes are opt-in hardening that only arrives if you copy it: the skeleton now sets session `serialization` to `json` and cache `serializable_classes` to `false`. Copying them has side effects (json invalidates active sessions), so decide deliberately. Conversely, any key your config omits takes the framework's value, so a changed framework default reaches you without a file change.
go deeper
Know that the upgrade guide lists changes by likelihood of impact and that config files in your app are your own copies.
Explain the split between laravel/framework and the laravel/laravel skeleton, and why framework defaults still reach keys your config omits.
Treat the skeleton diff as a list of decisions: adopt hardening such as json sessions deliberately, with a plan for its side effects.
Decide how closely each app should track the skeleton, and keep a record of intentional differences so every upgrade does not re-litigate them.
## Two things called "Laravel" A Laravel application is built from two separately versioned pieces: - **`laravel/framework`**, the Composer package in `vendor/`. Updating it changes framework code. - **`laravel/laravel`**, the skeleton the app was created from: `config/*.php`, `bootstrap/app.php`, `composer.json`, `phpunit.xml`, `.env.example` and so on. These files were **copied** into the repository once and are now the app's own code. Bumping `laravel/framework` to `^13.0` updates the first piece only. Nothing rewrites the app's copies of skeleton files. ## Reading the upgrade guide The Laravel 13 guide sorts every documented breaking change by **likelihood of impact**: | Impact | Examples in the 12 to 13 guide | |---|---| | High | dependency versions (`laravel/framework ^13.0`, `laravel/tinker ^3.0`, `phpunit/phpunit ^12.0`, `pestphp/pest ^4.0`, `laravel/boost ^2.0`); updating the installer; the CSRF middleware's new name | | Medium | cache `serializable_classes`; `upsert` on MySQL or MariaDB with an empty `uniqueBy` now throwing `InvalidArgumentException` | | Low / Very low | event property renames (`JobAttempted::$exception`, `QueueBusy::$connectionName`), new methods on contracts, the password-reset subject, `Js::from` output, Bootstrap 3 pagination view names | The guide's own estimate for this upgrade is **10 minutes**, because Laravel 13 was designed with minimal breaking changes. Contract additions matter only if the app implements those contracts itself, for example a custom cache store or queue driver. ## Why the skeleton diff matters The guide says it documents breaking changes in the framework and encourages you to review the skeleton's changes too, which it only partly covers. Comparing the skeleton's `12.x` and `13.x` branches shows config and bootstrap changes a new app would get. In Laravel 13 two of them are security hardening: - `config/cache.php` gains `'serializable_classes' => false`. The framework's own `config/cache.php` has no such key, so an upgraded app that does not add it keeps unserializing any cached object, as before. - `config/session.php` sets `'serialization' => 'json'`. The framework falls back to `php` when the key is absent. In both cases the protection arrives **only if you copy the change**, and copying has consequences: 1. Switching session serialization from `php` to `json` invalidates every active session, so users must sign in again. 2. Setting `serializable_classes` to `false` means cached PHP objects no longer come back as their classes; the guide says to list allowed classes or cache arrays instead. So the skeleton diff is a list of decisions, not a patch to apply blindly. ## The other direction: framework defaults Anything an app's config file omits is filled from the framework's own config files. When the framework changes one of those defaults, apps that never set the key pick up the change **without any file change**. The 13 guide gives an example: the fallback cache and Redis key prefixes now use hyphens, so an app relying on the fallback gets new keys (a cold cache after deploy) unless it sets `CACHE_PREFIX` and `REDIS_PREFIX` explicitly. ## Tests that need attention Several low-impact items show up first in the test suite rather than in production: assertions on the default password-reset subject (now "Reset your password"), comparisons of `Js::from` output that expected escaped Unicode, and tests that set custom `Str` UUID or ULID factories once and expected them to persist between test methods (they are now reset during teardown). Seeing these fail after the bump is normal and quick to fix. ## A checklist for the hop 1. Update `composer.json` versions from the guide's High section and run the suite. 2. Work through Medium and Low items that touch code the app actually uses. 3. Review the skeleton diff and decide, file by file, what to copy. 4. Check which config keys the app omits and whether their framework defaults changed. 5. Update tests that assert on changed strings or names. ## A permit portal example After moving the municipal permit portal to 13, the team diffs the skeleton and chooses to adopt `json` session serialization in a planned release with a banner warning that users will be signed out, and to add `serializable_classes` with the two value objects the portal caches.
- The portal's config/cache.php was created under Laravel 10 and has no serializable_classes key. What does Laravel 13 do with cached objects?It keeps unserializing them without a class restriction, as before. The cache manager passes the configured value and `null` means no allow-list; only the skeleton's new `config/cache.php` sets `false`. To get the hardening, add the key and list the classes the app really caches.
- How do you find the skeleton changes between two Laravel majors?Compare the `laravel/laravel` repository's branches for the two majors (for Laravel 13, `12.x...13.x`), as the upgrade guide suggests. It shows config, bootstrap, composer.json and test-config changes that a new app gets; decide for each whether to copy it.
saying these in an interview costs you the question
- Updating laravel/framework also updates the app's config files
- Every skeleton change is listed as a breaking change in the upgrade guide
- Copying the new session.php into an old app is always side-effect free
- An app with no serializable_classes key is hardened automatically in 13
- Contract additions affect every app, even those without custom implementations