Without the facade
The facade is the recommended default, not the only way in. There are three equivalent entry points, and they all run the same code:
- The Git facade — the shortest form, used throughout these docs.
- The manager — RoundlyConsulting\Git\GitManager, the container singleton and the facade root. Inject it through the constructor for the same API with an explicit dependency and no static calls.
- The driver layer — the flat, path-taking methods of the Interfaces\Provider contract that every handle method delegates to. It is the contract every driver implements and the fake doubles.
Injecting the manager
use RoundlyConsulting\Git\GitManager;
final class ShipRelease
{
public function __construct(private GitManager $git) {}
public function __invoke(string $repository, int $number): string
{
return $this->git->github()->repo($repository)->pullRequest($number)->merge();
}
}Git::fake() swaps the injected instance too — GitFake extends GitManager, so a class that type-hints the manager receives the recording fake in tests rather than a TypeError.
No action classes
Git is a remote-API client, so there are no action classes: the drivers are the use cases. The handles are thin — every handle method is the matching flat method on the driver with the path filled in — so you can also call that layer directly:
use RoundlyConsulting\Git\Enums\MergeMethod;
use RoundlyConsulting\Git\GitManager;
$github = app(GitManager::class)->github();
$github->mergePullRequest('acme/app', 12, MergeMethod::Squash); // same call as ->repo()->pullRequest(12)->merge()
$github->contents('acme/app', 'README.md', ref: 'main'); // same call as ->repo()->contents()
$github->listInstallations(); // same call as ->installations()->all()Handle method → driver method
| Handle method | Driver method (Provider contract) |
|---|---|
repo($p)->get() | repository($p) |
repo($p)->branches() / createBranch() | branches($p) / createBranch($p, NewBranch) |
repo($p)->commit($sha) / commits() | commit($p, $sha) / commits($p) |
repo($p)->pullRequests() / createPullRequest() | pullRequests($p) / createPullRequest($p, NewPullRequest) |
repo($p)->issues() / issue($n) / comment() | issues($p) / issue($p, $n) / comment($p, NewComment) |
repo($p)->tags() / createTag() | tags($p) / createTag($p, NewTag) |
repo($p)->releases() / release() / createRelease() | releases($p) / release($p, $tagOrId) / createRelease($p, NewRelease) |
repo($p)->contents() / createFile() / updateFile() | contents($p, $file, $ref) / createFile($p, NewFile) / updateFile($p, UpdatedFile) |
repo($p)->compare() / contributors() / languages() | compare($p, $base, $head) / contributors($p) / languages($p) |
repo($p)->webhooks()->register() / all() / delete() | createWebhook($p, NewWebhook) / listWebhooks($p) / deleteWebhook($p, $id) |
repo($p)->cloneUrl() | cloneUrlForRepository($p, $username, $credentials) |
pullRequest($n)->get() / close() | pullRequest($p, $n) / closePullRequest($p, $n) |
pullRequest($n)->merge() / approve() | mergePullRequest($p, $n, …) / approvePullRequest($p, $n, $body) |
pullRequest($n)->review() / reviews() | reviewPullRequest($p, $n, NewReview) / pullRequestReviews($p, $n) |
pullRequest($n)->comment($body) | comment($p, new NewComment($n, $body, CommentTarget::PullRequest)) |
installations()->all() / find($id) | listInstallations() / installation($id) |
installations()->forOrganization() / forUser() | organizationInstallation($org) / userInstallation($login) |
installations()->installUrl($state) | installUrl($state) |
The flat driver methods run the same scope checks as the handles, so they refuse the same out-of-scope paths. The fake records every call under the driver method it reached, whichever layer you called.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.