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

Phumbor Bundle Laravel Package

jbouzekri/phumbor-bundle

Symfony bundle integrating the Phumbor PHP client for Thumbor. Configure your Thumbor server and security key, then generate signed image URLs and use Twig helpers to resize, crop, and optimize images in templates. Simplifies wiring Phumbor services into Symfony apps for dynamic thumbnails.

View on GitHub
Deep Wiki
Context7

Getting Started

PhumborBundle is a thin Symfony bridge around the 99designs/phumbor client for Thumbor. It does one job: read server + secret from config and register the client's URL builder as a container service. There is no image processing here — all resizing/cropping/signing happens on your Thumbor instance.

1. Install

composer require jbouzekri/phumbor-bundle

Version 4.0.0 adds Symfony 8 support and drops Symfony versions that are no longer maintained. Practically: 4.x targets Symfony 8 (which itself requires PHP 8.4). If you are still on Symfony 6.x/7.x, stay on the 3.x line and pin it explicitly:

composer require "jbouzekri/phumbor-bundle:^3.0"   # older Symfony
composer require "jbouzekri/phumbor-bundle:^4.0"   # Symfony 8

Symfony Flex registers Jbouzekri\PhumborBundle\PhumborBundle automatically. On a non-Flex app add it to config/bundles.php:

Jbouzekri\PhumborBundle\PhumborBundle::class => ['all' => true],

2. Configure

# config/packages/phumbor.yaml
phumbor:
    server: '%env(THUMBOR_SERVER)%'   # public URL of your Thumbor / CDN, no trailing slash
    secret: '%env(THUMBOR_SECRET)%'   # must match SECURITY_KEY in thumbor.conf
# .env
THUMBOR_SERVER=https://thumbor.example.com
THUMBOR_SECRET=change-me

3. Confirm what got registered

bin/console debug:config phumbor
bin/console debug:container phumbor

The bundle wires the client's Thumbor\Url\BuilderFactory. Depending on the bundle version the service id is phumbor.builder (or phumbor.factory on older tags), and it may or may not be aliased for autowiring. The version-proof approach is to alias it yourself:

# config/services.yaml
services:
    Thumbor\Url\BuilderFactory:
        alias: phumbor.builder

4. First use case — a controller

use Thumbor\Url\BuilderFactory;

final class ProductController
{
    public function __construct(private BuilderFactory $factory) {}

    public function show(): Response
    {
        $thumbnail = $this->factory
            ->url('https://cdn.example.com/products/42.jpg')
            ->resize(400, 300)
            ->toUrl();

        return $this->render('product/show.html.twig', ['thumbnail' => $thumbnail]);
    }
}

Where to look first

  • vendor/jbouzekri/phumbor-bundle/ — the whole bundle is a few files (DependencyInjection/, Resources/config/services.*). Five minutes of reading tells you the exact service id and argument list for your version. Do this first after a major bump: 4.0 is a support-window release, so the service definition is the thing to re-verify, not the API.
  • The 99designs/phumbor README — the fluent builder API (resize, fitIn, crop, smartCrop, addFilter, toUrl, alignment helpers) lives there, not in the bundle docs.
  • A local Thumbor for experimentation: docker run -p 8888:8888 minimalcompact/thumbor then point THUMBOR_SERVER=http://localhost:8888.

Implementation Patterns

Wrap the builder in a domain service

Don't sprinkle $factory->url(...) across controllers and templates. Presets keep dimensions and filter stacks consistent and put the "which Thumbor" decision in one place:

final class ThumborUrls
{
    public function __construct(private BuilderFactory $factory) {}

    public function thumb(string $source): string
    {
        return $this->factory->url($source)->resize(150, 150)->toUrl();
    }

    public function hero(string $source): string
    {
        return $this->factory->url($source)
            ->fitIn(1600, 900)
            ->addFilter('quality', 80)
            ->toUrl();
    }

    public function og(string $source): string
    {
        return $this->factory->url($source)->resize(1200, 630)->toUrl();
    }
}

Inject ThumborUrls everywhere; the BuilderFactory stays an implementation detail of this one class. This wrapper is also what makes a future major-version bump (or a service-id change) a one-file edit.

A Twig function is the usual next step

The bundle is a container bridge — the presentation layer is yours to write:

final class ThumborExtension extends AbstractExtension
{
    public function __construct(private ThumborUrls $urls) {}

    public function getFunctions(): array
    {
        return [new TwigFunction('thumbor', [$this, 'render'])];
    }

    public function render(string $source, string $preset = 'thumb'): string
    {
        return $this->urls->{$preset}($source);
    }
}
<img src="{{ thumbor(product.image, 'thumb') }}"
     srcset="{{ thumbor(product.image, 'thumb') }} 400w,
             {{ thumbor(product.image, 'hero') }} 1600w"
     sizes="(max-width: 600px) 100vw, 800px"
     alt="{{ product.name }}">

Use filters, but verify them against your Thumbor build

$url = $factory->url($source)
    ->resize(300, 200)
    ->addFilter('quality', 80)
    ->addFilter('format', 'webp')   // filter availability depends on the Thumbor build
    ->toUrl();

Filters such as format, background_color, sharpen, and fill are supplied by Thumbor itself (or thumbor-plugins). If a filter name isn't recognised, Thumbor answers 400, not the bundle — test each filter you introduce against the real server, especially after upgrading Thumbor.

Normalise source URLs upstream

The builder takes whatever URL string you give it and signs it. Decide once whether you pass relative paths, absolute CDN URLs, or storage-loader paths (s3://…), and convert at a single boundary (a SourceUrlResolver service). Mixing conventions is the most common source of broken images.

Multiple Thumbor endpoints

The bundle supports one server/secret pair. If you need a second (regional CDN, separate secret per tenant), build a factory by hand rather than fighting the config:

$eu = new BuilderFactory('https://eu.thumbor.example.com', $euSecret);

Then register that as a named service and select between them in your wrapper.

Environments and the upgrade path

Point dev at a local/insecure Thumbor and prod at the CDN host. Keep the secret in a real secret store in prod; in dev an empty key is fine only if your Thumbor has unsafe URLs enabled — otherwise generate a throwaway key for the container too.

When moving to 4.0, do it as a two-step: bump Symfony to 8 (and PHP to 8.4) first, with the bundle still on 3.x if it allows, then bump the bundle. That way a BuilderFactory that fails to resolve is obviously attributable to the bundle, not the framework.

Testing

URL generation is pure string work, so assertions are straightforward:

self::assertStringContainsString('/400x300/', $urls->thumb($source));

For unit tests of services that consume ThumborUrls, mock the wrapper — not BuilderFactory — so you never depend on signature bytes. A single integration test in CI that boots the container and resolves Thumbor\Url\BuilderFactory is the cheapest guard against a service-id regression across bundle majors.

Gotchas and Tips

  • 4.0.0 is a support-window release, not a feature release. The only change of substance is Symfony 8 support plus removal of unsupported Symfony versions. There is no new API to learn, but there is a hard floor: check your symfony/* constraints and your PHP version before bumping, and check that nothing else in your dependency tree pins jbouzekri/phumbor-bundle to ^3. Composer will otherwise resolve to 3.x and you'll conclude the release "did nothing".
  • Re-verify the service id after the bump. The phumbor.builder / phumbor.factory naming has varied across tags before, and DependencyInjection/ is the only place that knows. Keep the explicit BuilderFactory alias in config/services.yaml so autowiring survives whatever the bundle does internally.
  • debug:container --show-private is gone. On Symfony 8 private services are listed by default, so bin/console debug:container phumbor is enough. Old runbooks that paste --show-private will now error out.
  • Secret mismatch is the #1 failure. A wrong or missing secret produces 403 from Thumbor with an "invalid security key" log line. Confirm phumbor.secret and SECURITY_KEY in thumbor.conf are byte-identical; restart the container after changing it.
  • Unsafe URLs. If Thumbor has no security key it expects /unsafe/… paths. The signed-URL builder won't emit them. Configuring a key on both sides is simpler than trying to force unsafe mode.
  • Trailing slashes in server yield https://host//… and confusing 404s. Store the URL without a trailing slash, and include any reverse-proxy path prefix (https://host/thumbor) since it becomes part of the signed path.
  • Source URLs with query strings must be encoded exactly once. Passing an already-encoded URL double-encodes it; Thumbor then fetches the wrong object. Encode at the boundary, nowhere else.
  • Env vars and config caching. The bundle turns your config into container parameters. After changing THUMBOR_SERVER/THUMBOR_SECRET in a deployed environment, run bin/console cache:clear (or warm the cache in the deploy) before concluding "it didn't pick it up." Symfony 8 makes no difference here, but the major bump is a good moment to confirm your deploy actually warms the cache.
  • Thumbor must be able to reach the source. http://localhost/... inside a Dockerised Thumbor resolves to the container itself. Use a public URL, a shared volume with a file loader, or host.docker.internal.
  • toUrl() does an HMAC per call. Cheap, but in loops over thousands of entities it adds up; cache generated URLs per request or per preset when rendering large grids.
  • Filter order and secret rotation affect CDN cache keys. Rotating the secret invalidates every signed URL, so schedule it with a cache purge — old URLs cached at the edge will start returning 403 only after they expire.
  • Autowiring is not guaranteed. If BuilderFactory type-hints fail, add the alias shown above rather than injecting ContainerInterface.
  • Extension points are deliberately minimal. To change behaviour, decorate or wrap the factory in your own service; avoid redefining phumbor.builder unless you have to. For extra config keys, extend the bundle's Configuration only as a last resort — a wrapper service is cleaner and survives bundle upgrades.
  • Small ecosystem, small surface. 26 stars and zero known dependents means you are effectively the documentation. The upside: the bundle is thin, MIT-licensed, and its entire behaviour fits in one screen of XML/PHP — read it once and you own it. Budget time accordingly, and pin the version in composer.json so an upstream change can't silently alter the service id.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect