Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Last Bundle Laravel Package

fw/last-bundle

fw/last-bundle is a compact Laravel/PHP package for managing a final bundle in your asset pipeline, helping ensure styles and scripts load in a predictable order with simple configuration and minimal overhead. It suits projects that need a clear last-loaded bundle without complex build changes.

View on GitHub
Deep Wiki
Context7

Getting Started

fw/last-bundle is a Symfony 4 bundle that freezes your app by running requests through the HttpKernel and writing the resulting HTML to disk — conceptually the same job as Jigsaw or spatie/laravel-export in Laravel-land, but reusing your real Symfony routes, controllers, and Twig templates instead of a separate site definition.

Minimal setup:

composer require fw/last-bundle

Symfony 4 Flex registers the bundle automatically; otherwise add it to config/bundles.php:

return [
    // ...
    Fw\LastBundle\LastBundle::class => ['all' => true],
];

Then, before writing any config, read these four files in vendor/fw/last-bundle/ — they tell you everything the README won't:

  1. DependencyInjection/Configuration.php — the exact config tree and defaults (this is the authoritative "docs" for a package this small).
  2. Command/ — the console command name and its options. Confirm with bin/console list last and, importantly, bin/console last:generate --help — as of v1.1 there is a new overwrite/force flag whose exact name you must verify before wiring it into CI.
  3. Resources/config/services.xml (or .yaml) — the service IDs you'll decorate or override.
  4. README.md — a working config sample you can paste.

A typical first config, in config/packages/last.yaml:

last:
    output_dir: '%kernel.project_dir%/build/static'
    # route names or paths to freeze, depending on what Configuration.php exposes
    urls:
        - { path: '/', name: 'home' }
        - { path: '/about' }
        - { path: '/blog', name: 'blog_index' }

First use case: freeze a marketing site or a blog index into build/static/, upload it to S3/Netlify/GitHub Pages, and keep the Symfony app alive only for the dynamic bits (contact form, search, admin). That gets you CDN-speed static delivery with zero change to your controllers.

In v1.1, expect the first run into a non-empty build/static/ to stop and warn you rather than silently clobbering whatever is already there — pass the force flag (or answer the prompt) once, then bake the flag into your scripts.

Implementation Patterns

Route enumeration is the whole game. Two approaches work:

  • Explicit list in last.yaml — deterministic, safe, but you must remember to add new pages.
  • Crawl-driven — seed with /, parse hrefs out of each response, and recurse within your own host. Cheaper to maintain, but it will happily wander into /admin and /logout if you don't guard it. Keep an allowlist of path prefixes and an explicit denylist.

For anything list-shaped (blog posts, product pages), generate the URL list from the database in a small console command or a KernelEvents subscriber rather than hand-maintaining YAML:

// src/Command/FreezeContent.php
$urls = $postRepository->findAllPublishedSlugs();
$this->filesystem->dumpFile(
    $this->projectDir.'/var/last-urls.json',
    json_encode($urls)
);

Then have your CI run FreezeContent immediately before the freeze command.

Fix the request context before you freeze. CLI-generated URLs default to http://localhost, so every absolute link in the output points at a host that doesn't exist. Set it explicitly:

# config/packages/last.yaml
last:
    request_context:
        host: 'www.example.com'
        scheme: 'https'
        base_url: ''

or, if the bundle doesn't expose it, set router.request_context.host / router.request_context.scheme parameters in services.yaml — the router honours those for both CLI and HTTP contexts.

Freeze inside a built container, not your dev box. The pipeline that has actually worked for teams:

APP_ENV=prod composer install --no-dev --optimize-autoloader
APP_ENV=prod bin/console cache:warmup
yarn encore production          # or assets:install, depending on your stack
APP_ENV=prod bin/console last:generate --force

Order matters: assets must exist before the freeze if the generator copies public/ alongside the HTML, and the Twig cache must be warm or you'll pay template compilation on every URL in the loop. The --force (verify the exact spelling with --help on v1.1) is mandatory in CI: without it the new overwrite guard will stop the run on a warm build/static/ directory, which is guaranteed to be non-empty on every build after your first.

Per-locale output. Loop locales in CI rather than in one process, so a failure in de doesn't leave you with a half-written en:

for locale in en de fr; do
  APP_ENV=prod bin/console last:generate \
    --output-dir=build/static/$locale \
    --locale=$locale --force
done

Clean the target directory yourself, then force. Now that the bundle asks permission before overwriting, the cleanest CI semantics are to rm -rf build/static && mkdir -p build/static as an explicit step and then always pass the force flag. That way deletion is visible in the pipeline log, you never ship stale pages from a deleted blog post, and you don't depend on the bundle's overwrite behaviour to remove orphans — a force flag overwrites files, it does not necessarily prune the directory.

Post-process the output. The generator gives you HTML; it usually won't give you the deployment niceties. Add a small script that: renames /foo → /foo/index.html for S3 static hosting, strips any _profiler or _wdt artefacts, and emits a sitemap.xml from the same URL list you fed the generator.

Split dynamic from static deliberately. Keep forms pointing at a real endpoint (action="https://api.example.com/contact"), not at the frozen page. Anything session- or CSRF-dependent will be dead on the CDN.

Gotchas and Tips

The v1.1 overwrite guard will hang or fail your CI if you ignore it. This is the headline behavioural change: on a non-empty output directory the command now warns instead of proceeding, and a new flag forces the overwrite. Two failure modes to watch for — an interactive prompt blocking a non-TTY build agent, and a non-interactive aborted run that (depending on how the command exits) may look like a green step in a loosely-written pipeline. Always pass the force flag explicitly in CI, and assert afterwards that build/static/index.html is newer than the build start so a skipped freeze can't pass silently. Also pin the version: composer require fw/last-bundle:^1.1 — teams on 1.0 won't have the guard, and teams that pull dev-master may get flag renames under them.

Maintenance risk is the first thing to evaluate. Last release August 2018, 11 stars, zero dependents (v1.1 is a small patch to a long-dormant bundle, not a revival). That means it targets Symfony 4 and almost certainly has symfony/framework-bundle: ^4.0 style constraints — it will not install on Symfony 5/6 without a fork or a replace in your root composer.json. Check vendor/fw/last-bundle/composer.json before you build a workflow around it. If you're on Symfony 4.4 LTS, budget a day to vendor the bundle into src/ (copy it, drop the LastBundle registration, keep the command and config) so you control the upgrade. Given the package's size, forking is genuinely cheaper than fighting constraints — and once forked, the overwrite guard is a five-line change you own.

Trailing-slash mapping and orphan files. The force flag controls whether existing files are rewritten; it's not a synchronisation step. Delete slugs you retire, or the next deploy ships a page you thought was gone. Decide /about vs /about/ up front, encode it in a post-processing step, and add a rewrite rule so both resolve — otherwise S3/Netlify 404 every deep link.

Non-HTML responses don't freeze cleanly. BinaryFileResponse, StreamedResponse, and redirects either blow up or write empty files. Filter them out of your URL list, or catch them in a wrapper that inspects $response->getStatusCode() and skips anything >= 300.

Watch memory across the loop. Freezing hundreds of URLs in one process reuses Doctrine's connection and the kernel's container; entity manager identity maps grow unbounded and you'll hit memory_limit around a few hundred pages. Call $em->clear() and $kernel->reboot(null) (or kernel.reset) every N requests inside a custom generator, or just chunk the URL list across multiple command invocations.

Debug mode leaks into output. APP_ENV=dev will bake the web debug toolbar markup into your static files. Always freeze with APP_ENV=prod APP_DEBUG=0, and diff one output file before and after to confirm.

Cache-busting is now your job. Once the HTML is static, ?v=123 query strings and Encore's manifest.json hashes determine whether browsers pick up new assets. Generate the manifest in the same build step as the freeze, and never hand-edit the frozen HTML.

Extension points to look for. Grep the bundle for EventDispatcher, tagged services (last.generator, last.writer), and interface-typed constructor arguments in the generator class. Decorating the generator service in services.yaml is usually the cleanest way to add URL discovery, response filtering, or post-write rewriting without forking:

services:
    App\StaticSite\FilteringGenerator:
        decorates: 'last.generator'
        arguments: ['@App\StaticSite\FilteringGenerator.inner']

A decorator is also the right place to add your own "directory is dirty, refuse to run" check if you want the safety of v1.1's guard but with a stricter policy (e.g. fail unless the checkout is clean).

Debug a bad page by request, not by command. Reproduce what the generator does with a quick controller-less call so you see the real exception instead of a swallowed one:

$request  = Request::create('/blog/some-post', 'GET', [], [], [], [
    'HTTP_HOST' => 'www.example.com',
]);
$response = $kernel->handle($request);
dump($response->getStatusCode(), $response->getContent());

If that renders correctly but the frozen file doesn't, the problem is in the writer, the output path, or the request context — not your application code. And if the file isn't written at all, check the overwrite prompt/flag before you debug anything else; post-1.1 that's the most common reason a working generator produces no output.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource