jenssegers/chef
Chef is a Laravel package that lets you define and run reusable "recipes" of artisan commands and PHP callbacks, so repetitive setup and maintenance tasks can be executed with a single command.
First, correct your mental model. The registry listing describes a Laravel "recipe" abstraction for Artisan-style tasks. That is wrong metadata. jenssegers/chef is a thin PHP client for the Chef Infra Server REST API. Its entire value is that it does Mixlib request signing — canonical request construction, body hashing, RSA signature, X-Ops-* headers — so you don't write that yourself. Everything else it gives you is a few Guzzle wrappers.
Minimal path to a working call, in order:
X-Ops-Sign 1.0 vs 1.1/1.3), which endpoints it actually wraps, and which Guzzle major it requires. That ten-minute read is more reliable than any documentation, including this one.composer require jenssegers/chef. The library predates Guzzle 7. If it resolves cleanly against your Laravel guzzlehttp/guzzle:^7 pin, good. If not, stop before writing integration code and decide fork-vs-sidecar.config/services.php, sourced from your secrets manager — never a committed .env that reaches CI artifacts:// config/services.php
'chef' => [
'server' => env('CHEF_SERVER_URL'), // https://chef.example.com/organizations/acme
'org' => env('CHEF_ORG', 'acme'),
'client' => env('CHEF_CLIENT_NAME'), // client or user name
'key_path' => env('CHEF_KEY_PATH'), // 0600 file, app-user owned
'timeout' => 5,
'connect_timeout' => 2,
],
Jenssegers\Chef\Chef appear in a controller or model.First real use case: node inventory sync behind a queued job. A scheduled job pages through nodes, upserts last_seen and convergence status into a chef_nodes table, and your dashboard reads the table instead of the Chef Server. It is read-only, bounded, and it forces you to get signing, config, pagination, and caching right — in that order, at low risk.
Wrap it behind an interface on day one. This is the single most important pattern. With zero dependents and no maintenance signal, the ability to fork, mock, or replace the implementation without touching product code is what makes the package usable at all.
// app/Contracts/ChefGateway.php
namespace App\Contracts;
interface ChefGateway
{
public function node(string $name): array;
public function nodes(int $start = 0, int $rows = 1000): array;
public function role(string $name): array;
public function triggerRun(string $node): void;
}
// app/Providers/ChefServiceProvider.php
public function register(): void
{
$this->app->singleton(ChefGateway::class, function ($app) {
$cfg = $app['config']['services.chef'];
return new PackageChefGateway(
new \Jenssegers\Chef\Chef($cfg['server'], $cfg['client'], $cfg['key_path'])
);
});
}
Keep the adapter (PackageChefGateway) thin — translation and error mapping only. That file is the only place that knows the package exists, so it's the only file you touch when you fork.
Every call goes through a queued job. The Chef Server is an external HTTP service you don't own; its latency and failure modes are not yours to fix. Node sync, convergence refresh, role reads — all jobs, with $tries, backoff(), and jitter. Nothing in a synchronous request path.
class SyncChefNodes implements ShouldQueue
{
public int $tries = 5;
public array $backoff = [10, 60, 300];
public function handle(ChefGateway $chef): void
{
$start = 0;
do {
$page = $chef->nodes($start, 1000);
$rows = $page['rows'] ?? [];
ChefsNode::upsertMany($rows); // chunk internally
$start += 1000;
} while (count($rows) === 1000);
ChefsNode::pruneMissingFrom($page['seen'] ?? []);
}
}
Batch and paginate; never fan out. One request per node will destroy you past a few hundred hosts. Page with start/rows (or the search equivalent), process in chunks, and treat disappearance as a first-class event — a node removed from Chef must be removed from your mirror, or inventory drifts silently.
Cache the slow-changing reads. Roles, environments, and data bags change weekly at most. A 5–15 minute TTL in Laravel's cache in front of the gateway takes the Chef Server out of nearly every request path and shrinks the failure surface.
return Cache::remember("chef.role.{$name}", now()->addMinutes(10), fn () => $chef->role($name));
Keep writes on a narrow, deliberate surface. Data-bag and role mutations and chef-client triggers are where an unmaintained library can hurt you. Each write should be an explicit, named job with an audit-log row, a validation that the target exists, and idempotency so a retry is safe. Do not expose write paths until the read path has run clean through a full release cycle.
Test against a fake Chef API, not a bearer-token stub. Chef auth is signature-based, so you can't fake it by setting a header. Stand up a test double that accepts any signed request and asserts on the canonical request your client built — that's where signing bugs live — or pin a pre-computed valid signature fixture.
Add a decorator for cross-cutting concerns. Wrap the gateway with retry/backoff, request logging, and timing metrics rather than subclassing the client:
class InstrumentedChefGateway implements ChefGateway { /* delegate + log + time */ }
That decorator survives a fork; subclassing package internals does not.
The metadata mismatch is a governance signal, not a typo. A listing that describes a completely different product tells you the project isn't curated. Pin an exact version, treat the source as the only documentation, and don't let registry metadata auto-approve anything.
Expect a Guzzle resolution fight. This library predates Guzzle 7 and its signing layer touches request options and handler stacks that shifted across majors. If Composer won't resolve, register your fork as a VCS repository, relax the constraint, and re-run the signing contract tests. Then add a CI guard: any composer update runs the Chef contract tests, so an unrelated dependency bump can't silently break auth.
A bare 401 is almost always one of three things:
X-Ops-Timestamp has roughly a 15-minute tolerance. Container clock drift breaks every call with no useful error. Enforce NTP and monitor drift as a metric.X-Ops-* headers at debug level — you debug this exactly once and you'll be glad it's in the log.The RSA private key is the credential. There is no token to revoke; if the key leaks, the Chef Server trusts it. Store it in a secrets manager or a 0600 file owned by the app user. Never in the repo, never in an .env that ships to a CI artifact. If you hold it in memory, know how long for.
Signing-version drift is the subtle killer. Chef Server accepts 1.0, 1.1, and 1.3, but the canonicalized string differs slightly between them. If the package emits 1.0 and your server rejects it, the error tells you nothing. Log the canonical request on the spike and compare against the spec.
Don't assume API coverage. A wrapper of this vintage typically covers node/role/environment/databag CRUD and possibly search, but the cookbook sandbox upload flow (checksum negotiation, multi-part upload) and policyfile endpoints are usually absent or partial. If cookbook publishing is on your roadmap, do it with knife in CI — that's the correct division of labour for a fork you own.
Set timeouts explicitly. Don't inherit Guzzle defaults on a client this old. Connect timeout of a couple seconds and a read timeout of five is right for inventory reads; long-running searches need their own job timeout. A hung Chef Server should fail a job, not a request.
Extension point is the client itself. There is no plugin system. You extend by wrapping (preferred — portable across forks) or subclassing (fragile). The decorator pattern in Implementation Patterns is the answer here.
Pin, vendor, own. Zero stars, zero dependents, no community pressure to keep it working on future PHP majors. Assume you are the maintainer from the moment composer require returns. Keep the fork in-repo, keep contract tests green, and book a periodic review alongside other tech-debt triage rather than discovering abandonment at the next PHP upgrade.
How can I help you explore Laravel packages today?