kzykhys/git
kzykhys/git is a lightweight PHP wrapper around the Git command-line tool, offering an object-oriented API for common repository tasks such as cloning, committing, branching, status, log, and diff. It requires Git installed but no PHP extension, making Git automation simple in PHP projects.
PHPGit is a thin, object-oriented wrapper around the git binary. It ships no Laravel service provider, no config file, and no facade — you wire it up yourself in about ten lines.
Install and verify
composer require kzykhys/git
git --version # the binary must exist on the PATH of the PHP process
Before anything else, confirm the Composer resolution actually worked — the last release is from Feb 2014 and pins an old symfony/process line, which will collide with the Symfony version Laravel ships. If composer require fails, resolve it first (fork + bump the constraint, or replace/alias in composer.json) rather than fighting it later.
The only two methods you need to learn first
use PHPGit\Git;
$git = new Git();
$git->setRepository(base_path()); // ALWAYS do this explicitly
$status = $git->status(); // ['branch' => ..., 'changes' => [...]]
setRepository() is the whole configuration surface: it tells every command which working tree to operate on. Under PHP-FPM the process CWD is public/, and under a queue worker it's whatever the daemon started in — never rely on the default.
Bind it as a singleton
// app/Providers/GitServiceProvider.php
public function register(): void
{
$this->app->singleton(Git::class, function () {
$git = new Git();
$git->setRepository(config('git.repository', base_path()));
return $git;
});
}
First use case: a deploy:pull artisan command or a GitHub/GitLab webhook controller that fetches and hard-resets a checkout. It is the shortest path from "I need to know what changed in this repo" to structured PHP data, and it beats shelling out by hand because you get escaping and exception handling for free.
$git->status() is __call; $git->branch->create('x') is __get returning a command object that is then invoked. The same verb is reachable both ways — pick one style per codebase and stick to it.
$git->checkout('main');
$git->checkout->create('feature/invoices'); // git checkout -b
$git->branch->delete('feature/old');
$branches = $git->branch(); // array of branch names
Most methods take ($arg1, $arg2, array $options). Booleans render bare flags, strings render --key=value:
$git->commit('chore: bump deps', [
'author' => 'Deploy Bot <[email protected]>',
'all' => true,
]);
$git->push('origin', 'main', ['u' => true]);
$git->merge('origin/main', ['no-ff' => true]);
$git->reset('origin/main', ['hard' => true]);
Argument order is not uniform across commands (push is remote-then-ref, diff is from-then-to, log takes a path or null first). Read the matching class in src/PHPGit/Command/ once rather than guessing — it's ~40 lines each.
public function handle(Git $git): int
{
$git->fetch('origin', ['prune' => true]);
$git->reset('origin/main', ['hard' => true]);
$this->info($git->log(null, ['limit' => 1])[0]['message']);
return self::SUCCESS;
}
Run it from a queued job with WithoutOverlapping, not from a request — fetch/clone block with no configurable timeout, and a slow remote will hang a PHP worker.
log, status, and blame return parsed arrays rather than raw stdout, which is the main reason to use this package over exec():
foreach ($git->log('app/Models', ['limit' => 20]) as $commit) {
$commit['hash']; $commit['author']; $commit['date']; $commit['message'];
}
dd() the first result in tinker before writing code against key names — they come from a hardcoded --format string inside the command class, not from a stable API contract.
Because there's no Laravel fake, testing against a real repository is the only built-in option. Put a narrow interface in front of it so unit tests never touch the binary:
interface Repository
{
public function head(): string;
public function isDirty(): bool;
public function commitAll(string $message): void;
}
Implement it with PHPGit in app/Support/GitRepository.php, and bind a fake in tests. Keep integration tests (real temp repo + real git) in a separate, slower suite.
composer require to fail on any modern Laravel. Plan for a fork with a widened constraint; do not spend an afternoon on dependency gymnastics for a 176-star package.clone is a reserved word. $git->clone(...) only parses on PHP 7+ (context-sensitive lexer). Fine today, but it means the package's own PHP 5.3 claim is misleading — and any static analysis or older tooling you run against it will choke.www-data needs read/write on the checkout, a HOME with the right .gitconfig, and the deploy key / known_hosts in place. Set GIT_SSH_COMMAND or GIT_TERMINAL_PROMPT=0 in the process env so a missing credential fails fast instead of hanging.safe.directory on git ≥ 2.35.2. If the repo directory is owned by deploy but the process runs as www-data, git refuses with "dubious ownership". Either chown, or run git config --global --add safe.directory /path as the PHP user.PHPGit\Exception\GitException. Catch it and log the message (stderr is folded in). Remember some commands legitimately exit 1 — diff --quiet, grep with no matches — so wrap those specifically or you'll treat "no changes" as an error.index.lock under concurrency. Two jobs in the same working tree will collide. Guard with Cache::lock("git:{$path}", 60) or WithoutOverlapping; the wrapper does nothing to serialize access.clone of a large repo or log of a huge history blocks. Keep long operations in jobs./^[A-Za-z0-9._\/-]+$/ and reject anything starting with - so it can't be read as a flag.stash or clean wrapper. For those, drop to Symfony\Component\Process\Process directly — Laravel already ships it — and keep the two styles clearly separated in your code.Git/AbstractCommand in app/Support/ or just write the one command you need with Process. Copying a 40-line command class is cheaper than patching the vendor directory.czproject/git-php or gitonomy/gitlib — both are maintained, both support modern PHP/Symfony, and neither will block your next Laravel upgrade.How can I help you explore Laravel packages today?