croct/plug-php
Croct PHP SDK brings dynamic, personalized content natively into your applications. Install via Composer and connect your Laravel or PHP app to Croct's headless CMS for content management, A/B testing, and personalization without complex integrations.
Install the SDK directly or via the Laravel bridge:
# Framework-agnostic
composer require croct/plug-php
# Laravel (recommended for Laravel apps — adds config, facade, middleware)
composer require croct/plug-laravel
For plain PHP, configure once during bootstrap:
use Croct\Plug\Croct;
Croct::configure([
'appId' => getenv('CROCT_APP_ID'),
'token' => getenv('CROCT_API_KEY'), // server-side only
]);
For Laravel, publish the config and set env vars instead:
php artisan vendor:publish --tag=croct
then set CRO CT_APP_ID and CROCT_APP_ID / CROCT_API_KEY in .env. The facade Croct is auto-registered.
Where to look first
docs.croct.com/reference/sdk/php/integration) — it walks through the app ID, token, and first slot.config/croct.php (Laravel) — the only place you should be flipping options like defaultTimeout, test, and storage/cache drivers.Croct\Plug\Croct class itself — configure(), personalize(), evaluate(), track(), identify() are the whole surface you'll touch day to day.First use case — personalize a single slot and render a fallback:
$hero = Croct::personalize('home-hero', [
'timeout' => 300,
'fallback' => ['title' => 'Welcome', 'cta' => '/signup'],
]);
This one call is the canonical "hello world": app configured, slot defined in the Croct dashboard, fallback in place so a network blip never breaks the page.
Configure in one place, use the facade everywhere. Put Croct::configure() in a singleton/service provider (or rely on plug-laravel's provider) and call the static facade from controllers, Blade components, and API resources. Never configure inside a request handler — it's meant to be idempotent bootstrap.
Slot-based personalization for SSR. The typical workflow is: define a slot in the Croct dashboard → call personalize($slotId, $options) in the controller → pass the returned array into a Blade view → ship a fallback object so the page renders if the API times out. Keep slot IDs in a constant/enum class rather than string literals scattered across templates.
Identify early, personalize later. Resolve the user once in middleware (Croct::identify($userId)) then reuse it:
$user = Croct::identify($user->id)
->withAttributes(['plan' => $user->plan, 'returning' => $user->isReturning()]);
$content = Croct::withUser($user)->personalize('dashboard-banner');
identify() gives Croct a stable audience key; withAttributes() lets your audience rules (plan, tenure, region) actually match. Do this in middleware so every downstream personalize()/evaluate() inherits the same user.
Evaluate expressions for flags and branches. For simple conditionals — feature flags, A/B gates — Croct::evaluate('user is returning') or Croct::evaluateExpression($expr) returns a scalar/bool. This avoids full slot round-trips when you just need a yes/no.
Track events alongside personalization. Conversions and CTA clicks should be Croct::track('cta_clicked', ['slot' => 'home-hero']). Fire-and-forget is fine; track() returns a promise you can ignore unless you need to await it. Keep event names snake_case and stable — they're the basis for audience rules.
Cache personalized payloads. In a high-traffic SSR app, wrap personalize() results in a PSR-16 cache keyed by a cacheKey that includes user audience + slot ID. Pass the cache option (and cacheKey) so Croct can skip the API when nothing has changed. This is the single biggest lever for latency in production.
Preview mode for content editors. Set previewToken (from the dashboard) on a staging environment so editors see unpublished variants. Keep it out of production config entirely.
Test mode / mocking. Use 'test' => true in non-production to avoid burning evaluation quota, and inject a fake plug (or wrap Croct behind a small interface) in unit tests so tests don't hit the network.
plug-php from a controller, that's fine; don't proxy it through JS.defaultTimeout globally and a shorter per-call timeout for above-the-fold slots. Always provide a fallback — a personalization miss should degrade, not 500.withAttributes() (or typoing a key) doesn't error — it just quietly never matches, so an empty {} response looks like a Croct bug. Log the attributes you send in staging.cacheKey. Also be wary of preview mode polluting the production cache.storage (the Laravel bridge handles this via middleware). Without it, identify() is effectively re-run every request.app()->environment('local', 'staging').logger option. Inject Laravel's logger (or Monolog) and set debug level while integrating — timeouts, auth failures, and schema mismatches all surface there but are otherwise swallowed as fallbacks.logger, clock, storage, cache, previewToken, defaultTimeout, and test are all injected via configure(). If you need to mock time or swap the cache, this is the seam — subclass/inject there rather than wrapping the facade.composer.json, and treat upgrades as a review item (especially around the personalize() options schema, which has evolved).personalize() call can return null/empty when no audience matches; that's not an exception, it's "you haven't defined a variant for this user." Handle it with the same fallback path as a timeout.How can I help you explore Laravel packages today?