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

Bitbucket Laravel Package

graham-campbell/bitbucket

A Laravel/PHP package by Graham Campbell that provides a clean, configurable Bitbucket Cloud API v2 client, with service provider and facade integration for working with repositories, pull requests, users, webhooks, and related resources.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish.

composer require graham-campbell/bitbucket
php artisan vendor:publish --provider="GrahamCampbell\Bitbucket\BitbucketServiceProvider"

Auto-discovery registers the service provider and the Bitbucket facade alias; the publish step drops config/bitbucket.php into place. If you are on a pre-auto-discovery Laravel version (you should not be, given the v11.1.0 matrix), register provider and alias manually.

Configure one connection. The config defines a default connection plus a connections map. Pick your auth mode per connection:

  • App password (fastest for server-side jobs): driver = app password, with the Bitbucket username and the generated app password. Not your account password, and not your email — Bitbucket app passwords pair with the username.
  • OAuth 2 (needed for anything acting on behalf of a user, or for refreshable long-lived tokens): consumer key/secret plus token and refresh_token.

Drive it from env (BITBUCKET_DRIVER, BITBUCKET_USERNAME, BITBUCKET_PASSWORD, BITBUCKET_CLIENT_ID, BITBUCKET_CLIENT_SECRET, BITBUCKET_TOKEN, …) so config:cache works unchanged in production.

First use case: prove the plumbing with a read-only command. Write one artisan command that lists open pull requests for a repository and prints id/title/author:

$client = Bitbucket::connection(); // default connection from config
$prs = $client->pullRequests()->list($workspace, $repo, ['state' => 'OPEN']);
foreach ($prs as $pr) {
    $this->line("#{$pr['id']} {$pr['title']}");
}

That single command validates credentials, network/egress, scopes, and pagination handling at once — which is exactly the set of things that fails first. Once it works against a non-production workspace, you have your template for every other endpoint.

Where to look first. The facade resolves to the package's manager; Bitbucket::connection('name') hands you an instance of Bitbucket\Client from bitbucket/client. The package itself is thin — the surface you will actually live in is the SDK's resource classes (repositories(), pullRequests(), issues(), pipelines(), webhooks()), so keep the SDK docs open and let your IDE inspect the resource objects rather than guessing method signatures.

Implementation Patterns

Wrap the SDK behind your own interface — always. This is the single highest-leverage decision you make on day one. Application code should never import Bitbucket\Client directly; the next bitbucket/client major then costs you one class, not a codebase sweep.

interface BitbucketGateway
{
    public function openPullRequests(string $workspace, string $repo): array;
    public function postBuildStatus(string $workspace, string $repo, string $sha, string $state): void;
}

final class SdkBitbucketGateway implements BitbucketGateway
{
    public function __construct(private \Bitbucket\Client $client) {}

    public function openPullRequests(string $workspace, string $repo): array
    {
        $out = [];
        foreach ($this->client->pullRequests()->list($workspace, $repo, ['state' => 'OPEN']) as $pr) {
            $out[] = ['id' => $pr['id'], 'title' => $pr['title'], 'author' => $pr['author']['display_name'] ?? null];
        }
        return $out;
    }
}

// AppServiceProvider::register()
$this->app->singleton(BitbucketGateway::class, fn ($app) =>
    new SdkBitbucketGateway($app->make(\GrahamCampbell\Bitbucket\BitbucketManager::class)->connection())
);

Controllers, jobs, and commands type-hint BitbucketGateway. Swapping to a raw-SDK implementation, a fake, or a different provider becomes a container binding.

Push all writes and bulk reads onto the queue. Bitbucket's rate limits and pagination are the throughput ceiling, not the package. Any operation that enumerates repos, PRs, or commits should be a job with explicit retry/backoff, not a synchronous call in a request:

final class SyncPullRequests implements ShouldQueue
{
    public int $tries = 5;
    public array $backoff = [10, 60, 300, 900];
    public int $timeout = 120;

    public function handle(BitbucketGateway $gateway): void { /* … */ }
}

Persist a cursor (last synced timestamp or page token) so a failed job resumes rather than restarts.

Treat pagination as a first-class concern. The SDK's list calls return paginated collections. Iterate them; do not index into a single response and assume you got everything. If you need bounded work, paginate deliberately and record the next cursor. A dashboard that shows "the first 10 PRs" is a bug report waiting to happen on a busy repo.

Verify webhooks yourself. The package is an outbound API client; inbound webhook verification is your code. Bitbucket signs payloads with X-Hub-Signature (HMAC-SHA256 over the raw body):

$raw = $request->getContent();
$expected = 'sha256='.hash_hmac('sha256', $raw, config('services.bitbucket.webhook_secret'));
abort_unless(hash_equals($expected, (string) $request->header('X-Hub-Signature')), 403);

ProcessBitbucketEvent::dispatch($request->header('X-Event-Key'), $raw);

Verify against the raw body, not $request->all() re-encoded — JSON re-encoding breaks the digest. Then hand off to a job and return 202 immediately; Bitbucket will retry on non-2xx and you do not want a slow handler causing duplicate deliveries.

Cache reads, not permissions. Repo lists, workspace members, and branch lists change slowly and are read on every page load of a dashboard. Cache::remember("bb:repos:{$workspace}", 300, fn () => ...) cuts your API calls by an order of magnitude. Never cache the result of a permission-dependent call without keying the cache by the acting identity — a cached PR list from an admin token served to a restricted user is a data-leak bug, not a performance win.

Use named connections for distinct identities. A bot connection for automation and a user-delegated OAuth connection for interactive features. Bitbucket::connection('bot') and Bitbucket::connection('user') keep scopes and rate-limit budgets separated.

Fake the gateway in tests, not HTTP. Because the SDK uses its own HTTP client, Http::fake() will not intercept calls. Mock your BitbucketGateway binding in feature tests. For genuine SDK-level contract tests, inject a Guzzle client with a MockHandler into the SDK instance — but those belong in a small, separate suite that runs against recorded fixtures, not in the main test path.

Gotchas and Tips

  • Http::fake() silently does nothing here. The SDK speaks Guzzle directly, so Laravel's HTTP client fakes are bypassed and your "unit" test makes real network calls. This is the most common false-positive in a first implementation. Mock your gateway interface instead.
  • OAuth token refresh is not magic. The package wires credentials into the SDK; it does not persist rotated refresh tokens for you. If you are on OAuth, you own the refresh flow: detect expiry/401, refresh, store the new token pair atomically, and retry once. Two concurrent workers refreshing the same token is a classic production incident — serialize it with a lock (Cache::lock).
  • A 404 usually means "you cannot see it." Bitbucket returns 404 rather than 403 for private repos the token cannot access. Do not write your error handling assuming 404 means "deleted"; log the workspace/repo/slug alongside the status or you will chase ghosts.
  • Respect Retry-After on 429. Catch rate-limit responses explicitly, honour the header, and let job backoff do the rest. Retrying immediately turns a throttle into an outage. Instrument 429 rate as a first-class metric — exception volume alone hides it.
  • config:cache is a plaintext secret at rest. The published config is env-driven, which is correct, but remember the cached file contains the resolved consumer secret and app password. Restrict file permissions, keep it out of image layers you publish, and php artisan queue:restart after any credential change — long-running workers hold the old config in memory.
  • Set timeouts explicitly. Default Guzzle timeouts are effectively "hang forever" for a request-path call. Put timeout and connect_timeout in the connection config, and never call the API synchronously from a web request without them.
  • The bitbucket/client version floor moves on minor wrapper releases. v11.1.0 requires ^5.1 on what is nominally a minor bump. If anything else in your tree pins the SDK to 4.x, composer update will either fail or silently re-resolve. Run composer why-not bitbucket/client ^5.1 before scheduling the upgrade, and check your call sites for catch blocks and type hints against SDK exception classes and pagination iterators — those are what a 4→5 bump actually breaks.
  • Widened constraints are not tested constraints. The release notes add PHP 8.5 and Laravel 13 support. That is additive and good, but widen-and-forget means Composer will happily resolve you into a combination nobody ran. Confirm against the package's CI matrix before making it your production assumption.
  • The facade is statically typed as a manager, not a client. Bitbucket::connection() returns a fresh-looking object but connections are cached by the manager, so any state you mutate on the client (default headers, custom middleware) leaks across callers in the same process. Prefer per-call configuration over mutating the shared client.
  • Repeated Bitbucket::connection() calls inside a loop are free but not harmless — they are the same instance, so concurrent-ish state (last request id, injected auth token) is shared. In queue workers this is usually fine; in Octane it is a genuine cross-request bleed risk. If you run Octane, resolve the client per request or reset it in a middleware.
  • Extension point is the container, not the package. There is no deep plugin surface here — the intended escape hatches are publishing the config (per-connection settings), binding your own gateway, and swapping the SDK client instance. Reach for those rather than fighting the manager.
  • Keep a fork path warm. Single-maintainer, zero-dependents reality. Because the package is ~a few hundred lines of wiring over the official SDK, the mitigation is trivial: know that Bitbucket::connection() is essentially new Bitbucket\Client() plus auth, so a fork or an in-house replacement is an afternoon, not a project. Pin the version in your dependency register and review it on the same annual cadence as your framework upgrades.
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.
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
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