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

Gitlab Laravel Package

graham-campbell/gitlab

A Laravel integration for the GitLab API v4 client, providing a service provider, facade, and simple configuration so you can query projects, issues, merge requests, and other GitLab resources from your PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bridge and publish its config:

composer require graham-campbell/gitlab
php artisan vendor:publish --tag=config

Laravel's package auto-discovery registers the service provider and the GitLab facade alias, so there is nothing to add to config/app.php. Lumen users must register GrahamCampbell\GitLab\GitLabServiceProvider manually and enable facades.

Populate credentials. For gitlab.com you only need a token; for self-managed you also need the API base URL:

GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
GITLAB_URL=https://gitlab.com/api/v4

Read these three things, in this order:

  1. config/gitlab.php — a default connection name plus a connections array. That file is the entire opinion surface of the package; everything else is delegation.
  2. src/GitLabManager.php and src/GitLabFactory.php — roughly 100 lines total. They build a GitLab\Client from a config array, memoize it per connection name, and hand it back. Fifteen minutes and you know the package completely.
  3. The upstream client docs and source (gitlabphp/api-client), not this README. Method names, endpoint coverage, response shapes, and pagination all live there. The bridge cannot expose anything the client doesn't.

First use case — read-only, zero blast radius. A "my open merge requests" panel on an internal dashboard:

use GrahamCampbell\GitLab\Facades\GitLab;

$mrs = GitLab::mergeRequests()->all($projectId, [
    'state'    => 'opened',
    'scope'    => 'assigned_to_me',
    'per_page' => 50,
]);

$pipeline = GitLab::pipelines()->all($projectId, ['per_page' => 1])[0]['status'] ?? 'unknown';

Point it at a scratch project first. A bad read produces an empty widget; a bad write produces a mutated customer repository. Earn the write path.

Implementation Patterns

Put the facade behind an application-owned adapter. This is the single most important pattern here. Never let GitLab:: appear in a controller, job, Blade view, or Livewire component. Confine it to one class and inject that everywhere.

namespace App\Integrations\GitLab;

use GrahamCampbell\GitLab\Facades\GitLab;

interface GitLabPort
{
    public function openMergeRequests(int $projectId): array;
    public function failingPipelines(int $projectId): array;
}

class FacadeGitLab implements GitLabPort
{
    public function __construct(private ?string $connection = null) {}

    private function client()
    {
        return $this->connection
            ? GitLab::connection($this->connection)
            : GitLab::getFacadeRoot();
    }

    public function openMergeRequests(int $projectId): array
    {
        return $this->client()->mergeRequests()->all($projectId, [
            'state' => 'opened', 'per_page' => 100,
        ]);
    }
}

Name methods after domain verbs, not GitLab resources (failingPipelines, not pipelinesAll). When you later swap to raw Guzzle, GraphQL, or the client directly, the diff is one class.

Multiple connections are the actual differentiator. The manager resolves named connections from config, which is what makes multi-tenant and self-hosted SaaS deployments cheap:

'connections' => [
    'main' => ['token' => env('GITLAB_TOKEN'), 'url' => env('GITLAB_URL')],
    'acme' => ['token' => env('GITLAB_ACME_TOKEN'), 'url' => 'https://gitlab.acme.internal/api/v4'],
],

Resolve the name from the tenant record, never hardcode it:

app(GitLabPort::class, ['connection' => $tenant->gitlab_connection])->openMergeRequests($id);

Pagination has two modes and you should decide explicitly per call site.

// Shallow list: max out the page and accept truncation
GitLab::connection()->setPerPage(100);

// True "all": traverse with the upstream pager
$pager = new \GitLab\ResultPager(GitLab::connection());
$all = $pager->fetchAll(
    GitLab::connection()->issues(), 'all', [$projectId, ['state' => 'opened']]
);

Sequence features deliberately: read-only list → cached read → queued bulk sync → single write action → batched writes. Bulk operations (every MR in a group, syncing hundreds of issues) belong in jobs with WithoutOverlapping and RateLimited middleware. Never in a request cycle.

Cache the slow stuff. Project metadata, member lists, label sets, and milestone definitions change rarely. Cache them with a short TTL keyed by connection + project + resource; dashboards that fan out to five GitLab calls per page load will otherwise get you 429'd by gitlab.com.

Testing. The facade is mockable like any Laravel facade:

GitLab::shouldReceive('mergeRequests->all')
    ->once()->with(42, Mockery::type('array'))
    ->andReturn([['iid' => 1, 'title' => 'Fix flaky spec']]);

Prefer binding a fake GitLabPort in the container so feature tests never touch the package at all. GitLab::extend('fake', fn () => $mockClient) is the escape hatch for driver-level stubs.

Log the request ID. On any failure, capture the rate-limit headers and X-Request-Id from the response. When a dashboard dies in production, GitLab-side admins will ask for that ID and nothing else will do.

Gotchas and Tips

  • config:cache freezes your env() calls. After php artisan config:cache, the token inside config/gitlab.php is baked in at cache time. Rotate a secret in your secret manager and you get a 401 in production with no code change. This is the #1 "works locally" failure. Either re-cache on rotation or register a custom connection that reads the secret at runtime.
  • The bridge adds no retries, no backoff, no rate limiting. A 429 or a transient 502 propagates straight into your controller. Wrap calls: retry(3, fn ($e) => $e->getCode() === 429, ...), honor Retry-After, and never let a retry loop sit in a synchronous request.
  • Errors are upstream exceptions. Expect GitLab\Exception\RuntimeException plus Guzzle's RequestException and ConnectException. Catch and translate at the adapter boundary so package exception types never leak into your domain or your API responses.
  • ->all() silently truncates to the page size — 20 by default. Getting 20 rows where you expected 300 is the classic bug. Always pass per_page explicitly, and reach for ResultPager whenever "all" genuinely means all.
  • Token hygiene. Long-lived PATs end up in .env, in cached bootstrap files, and in somebody's dd(config('gitlab')). Never dump the config array — log the connection name. Scope tokens to read_api for dashboards and reserve api scope for the one service that genuinely writes.
  • Coverage is inherited, not negotiated. Before promising a feature, grep the upstream client for the endpoint. When it's missing, drop one layer: GitLab::connection()->getHttpClient() gives you a Guzzle client already configured with the base URL and auth, so you can hand-roll the request without abandoning the config plumbing. GraphQL is entirely out of scope.
  • GitLab::extend('name', $callable) registers a custom driver on the manager. Use it for a pre-authenticated client, a proxy-configured Guzzle stack, or a test double — it expresses things the config array can't.
  • Guzzle / PSR-7 version conflicts are the most common install failure, especially alongside other HTTP-heavy packages. Resolve dependencies in a single composer update pass; don't add this mid-release.
  • Not a webhook or OAuth solution. Inbound event ingestion (X-Gitlab-Token verification, job payload parsing) and "Sign in with GitLab" (use Socialite) are yours to build. The package name creates this false expectation — flag it early rather than at sprint review.
  • Write-path discipline. Search-before-create to keep issue and MR creation idempotent, add a dry-run flag, record returned id/iid values for audit, and never let an untested loop iterate over production projects. Support a "write enabled" feature flag per connection so a misconfigured tenant can't mutate anything.
  • Pin and watch. Single-maintainer project tracking Laravel majors and upstream client releases. Pin the version, let Dependabot surface bumps, and schedule Laravel upgrades as real work. Because the dependency sits behind your adapter, abandonment costs days rather than weeks — but only if you enforced that boundary from day one.
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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle