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

Fz Hosting Symfony Bundle Laravel Package

formatz/fz-hosting-symfony-bundle

The FormatZ FZ Hosting Symfony Bundle integrates FZ hosting services into Symfony applications, providing bundle configuration and service wiring to streamline setup, dependency injection, and hosting-related workflows for Symfony developers.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you run composer require, confirm what you actually have. This is a Symfony bundle (fz-hosting-symfony-bundle), not a Laravel package. Laravel has no bundle loader — it will not auto-register the Bundle subclass, the DI extension, the configuration tree, or compiler passes. The first five minutes should be spent deciding whether you are integrating it or mining it, not installing it.

Where to look first, in this order:

  1. composer.json — the php floor and the symfony/* constraints. This is the single biggest predictor of friction against a Laravel 11/12 lockfile.
  2. LICENSE — it is not stated in the package metadata. If it is missing or proprietary, go no further until legal signs off.
  3. src/*Bundle.php, DependencyInjection/*Extension.php, DependencyInjection/Configuration.php — the configuration keys you'll need to translate into a Laravel config file.
  4. The client/service classes (likely under src/Client, src/Service, src/Http) — this is the part with day-to-day value.
  5. Entity/ or Resources/config/doctrine — Doctrine mappings are a hard stop for porting; Laravel is Eloquent.

Your first use case should be read-only. Pick one operation that only fetches state (list domains, fetch account details, list services) and prove it against FZ's sandbox. Do not start with provisioning or account mutation.

If the domain layer is framework-agnostic (plain classes, PSR-18/PSR-7 HTTP, PSR-3 logging, no ContainerBuilder references in the business logic), the fastest path to a working spike is a throwaway Symfony app, not your Laravel app:

// config/bundles.php in the spike only
return [
    Fz\Hosting\FzHostingBundle::class => ['all' => true],
];
# config/packages/fz_hosting.yaml in the spike only
fz_hosting:
    api_key: '%env(FZ_HOSTING_API_KEY)%'
    # keys come from the bundle's Configuration tree — read it, don't guess

Then exercise it from a bin/console REPL or a scratch command. You are learning the API contract here, not building a product — the spike gets deleted once you've recorded request/response shapes.

If the domain layer is Symfony-coupled (services registered via compiler passes, lifecycle handled by kernel.event_subscriber, entities owning tables), stop porting and plan for a sidecar. See the next section.

Implementation Patterns

Pattern A — Sidecar (default recommendation)

When the bundle owns state, controllers, or Doctrine schema, do not port it. Run the bundle in a small Symfony service, and call it from Laravel over HTTP.

// app/Jobs/ProvisionHostingAccount.php
final class ProvisionHostingAccount implements ShouldQueue
{
    use Queueable, InteractsWithQueue;

    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120, 300];

    public function __construct(public readonly int $orderId) {}

    public function handle(): void
    {
        $idempotencyKey = "provision-order-{$this->orderId}";

        $response = Http::baseUrl(config('hosting.sidecar_url'))
            ->withToken(config('hosting.sidecar_token'))
            ->withHeaders(['Idempotency-Key' => $idempotencyKey])
            ->retry(3, fn (int $attempt) => 1000 * $attempt, throw: false)
            ->timeout(20)
            ->post('/api/provision', ['order_id' => $this->orderId]);

        if ($response->serverError() || $response->connectionError()) {
            $this->release(60); // degrade to "pending", never 500 the user
            return;
        }

        $response->throw();

        event(new HostingAccountProvisioned($this->orderId, $response->json()));
    }
}

Enforce a concurrency cap on this queue (->onQueue('hosting') plus a Redis limiter) so a provisioning storm doesn't trip FZ's upstream throttles.

Pattern B — Extract the client (best if the source is clean)

If the HTTP layer is PSR-compatible and free of DI, copy it into your own namespace and bind it through a service provider. This is the pattern that gives you a stable seam you control regardless of the upstream package's fate.

// app/Providers/HostingServiceProvider.php
public function register(): void
{
    $this->mergeConfigFrom(__DIR__.'/../../config/hosting.php', 'hosting');

    $this->app->singleton(HostingClient::class, fn ($app) => new HostingClient(
        http: $app->make(Factory::class)->withToken(config('hosting.api_key'))
            ->baseUrl(config('hosting.base_url'))
            ->timeout(20)
            ->retry(3, 200, throw: false),
        logger: $app->make(LoggerInterface::class),
    ));

    $this->app->bind(HostingClientInterface::class, HostingClient::class);
}

public function boot(): void
{
    $this->publishes([__DIR__.'/../../config/hosting.php' => config_path('hosting.php')], 'hosting-config');
}

The key move: define your own HostingClientInterface first, then make the extracted code one implementation behind it. Swap in a fake in tests, and the upstream bundle becomes a reference implementation rather than a dependency.

Pattern C — Spike-only

Throwaway Symfony app, record every request/response pair into fixtures, discard the bundle, write a 300-line client of your own. Correct when the surface area is small — which, given zero dependents, is likely.

Translating Symfony config to Laravel config

Map the bundle's Configuration tree node-for-node into config/hosting.php, reading secrets from the environment only:

// config/hosting.php — mirrored from the bundle's Configuration tree
return [
    'base_url' => env('FZ_HOSTING_BASE_URL', 'https://api.fz-hosting.example'),
    'api_key'  => env('FZ_HOSTING_API_KEY'),
    'timeout'  => env('FZ_HOSTING_TIMEOUT', 20),
    'sandbox'  => env('FZ_HOSTING_SANDBOX', true),
];

Keep the key names aligned with upstream so the mapping stays obvious when you diff against a future release.

Events and commands

The bundle's lifecycle events are Symfony EventDispatcher events and will not fire in Laravel. Two honest options: (a) the sidecar re-emits them as HTTP webhooks that Laravel receives and converts into native events, or (b) your adapter dispatches Laravel events at the call sites. Never try to bind Symfony's dispatcher into Laravel's container.

Console commands translate cleanly to Artisan — they're usually thin wrappers over the client, so wrap your HostingClientInterface calls:

class SyncDomainsCommand extends Command
{
    protected $signature = 'hosting:sync-domains {--dry-run}';

    public function handle(HostingClientInterface $client): int
    {
        $domains = $client->listDomains();

        if ($this->option('dry-run')) {
            $this->table(['domain', 'status'], $domains->map->only(['name', 'status']));
            return self::SUCCESS;
        }
        // persist via Eloquent, reconcile against local state
        return self::SUCCESS;
    }
}

Testing

Because the upstream has no test suite you can lean on, your tests are your contract. Use Http::fake() against recorded fixtures from the spike and assert on outbound shape (headers, idempotency key, payload), not just on the happy path:

Http::fake([
    '*/api/domains' => Http::response($this->fixture('domains.json')),
]);

$client = app(HostingClientInterface::class);
$this->assertCount(3, $client->listDomains());

Http::assertSent(fn ($req) => $req->hasHeader('Authorization'));

Gotchas and Tips

Never install this into the Laravel application. Laravel's composer.lock pins Symfony components tightly; a bundle carrying symfony/http-kernel: ^4.4|^5.4 constraints will produce unresolvable conflicts even if it is never booted. Any usage should live in the sidecar or in extracted code with no Composer edge back to the package.

Private GitLab, not Packagist. If there is no Packagist entry, adding it as a VCS repositories entry requires a deploy token wired into CI — and that token is now part of your build's blast radius. Rotate it on a schedule and scope it to read-only.

No tags means no versions. If there are no releases, pin to a commit SHA in composer.json, not dev-main. A branch reference can change under you between two builds of the same code.

Extraction is not a licence bypass. If the licence turns out to be missing or restrictive, copying classes into app/Services is still copying. Get the answer in writing before anyone opens an editor.

Compiler passes and tagged services do not transplant. If the bundle's wiring is load-bearing — services collected via tags, container parameters read at compile time — you cannot reproduce that in Laravel's container without re-deriving it by reading the pass. Budget the reading time; there is no documentation to shortcut it.

Doctrine is a hard stop. Entities, mappings, and migrations mean the bundle assumes it owns tables. Do not adopt it into Laravel and try to share a database. Run it in the sidecar with its own schema, or extract only the non-persistent client.

Idempotency is your responsibility. Hosting provisioning endpoints are rarely idempotent by default — a retried request can create a duplicate domain or certificate. Always send an idempotency key and run a nightly reconciliation job comparing FZ's state against your local records.

Treat auth failures as a distinct error class. Token expiry looks like a 401/403, not a transport failure — alert on it separately, and never let a stale credential surface as a generic queued-job failure with no signal.

Debugging a sidecar is two-log debugging. Laravel job IDs will not appear in Symfony logs unless you pass a correlation ID in a header and log it on both sides. Do that from day one; retrofitting it during an incident is miserable.

You are the reviewer. Zero stars and zero dependents means nobody else has production-tested this. Read the client code end to end — particularly how it handles retries, timeouts, and errors — before it touches a request path.

The durable seam is yours, not theirs. Whatever path you take, the thing that survives is HostingClientInterface defined in your codebase. Everything upstream — the bundle, the client class you copied, the sidecar's routes — becomes replaceable behind it. If you only take one thing from this assessment, take that.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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
cgv/localized-resource