woduda / civicrm-laravel
Thin Laravel adapter around woduda/civicrm-php (PSR-18 CiviCRM APIv4 client)
Requires
- php: ^8.3
- illuminate/contracts: ^11|^12|^13
- illuminate/support: ^11|^12|^13
- spatie/laravel-package-tools: ^1.16
- symfony/yaml: ^7|^8
- woduda/civicrm-php: ^0.7
Requires (Dev)
- guzzlehttp/guzzle: ^7
- larastan/larastan: ^3
- laravel/pint: ^1.18
- nyholm/psr7: ^1
- orchestra/testbench: ^9|^10|^11
- pestphp/pest: ^3
- pestphp/pest-plugin-laravel: ^3
- php-http/mock-client: *
- phpstan/phpstan: ^2
- rector/rector: ^2
README
Thin, idiomatic Laravel adapter around woduda/civicrm-php —
a PSR-18 CiviCRM APIv4 client. Provides container bindings, a CiviCrm facade, queueable idempotent
jobs, an optional transactional outbox, artisan commands, webhook verification middleware, and a
CiviCrm::fake() test double.
Requires PHP ≥ 8.3, Laravel 11 / 12 / 13.
Quickstart
1. Install
composer require woduda/civicrm-laravel
Laravel's package auto-discovery registers the service provider and CiviCrm facade automatically.
2. Configure .env
CIVICRM_BASE_URL=https://your-site.example.org/civicrm/ajax/api4/ CIVICRM_API_TOKEN=your_civicrm_api_key # CIVICRM_SITE_KEY=optional_site_key
Publish the config file to customise queue, webhook, and retry settings:
php artisan vendor:publish --tag=civicrm-config
3. Verify the connection
php artisan civicrm:test-connection
# OK https://your-site.example.org/civicrm/ajax/api4/ (42 ms)
4. Use the facade
use CiviCrm\Laravel\Facades\CiviCrm; use Woduda\CiviCRM\Query\GetQuery; $contacts = CiviCrm::contacts()->get( GetQuery::new()->where('email', \Woduda\CiviCRM\Query\Operator::Equals, 'alice@example.org') ); foreach ($contacts as $contact) { echo $contact->displayName; }
Queued Jobs
SyncContactJob — idempotent contact upsert
Dispatching the job enqueues an upsert that:
- Looks up the contact by
externalIdentifier(if provided) or by email viaupsertByEmail - Creates or updates the contact with the provided fields
- Applies tags, groups, and custom fields in post-upsert steps
use CiviCrm\Laravel\Data\ContactInput; use CiviCrm\Laravel\Jobs\SyncContactJob; // Minimal — match by email dispatch(new SyncContactJob(ContactInput::fromArray([ 'email' => 'alice@example.org', 'firstName' => 'Alice', 'lastName' => 'Smith', ]))); // Full — match by externalIdentifier, with tags, groups, and custom fields dispatch(new SyncContactJob(new ContactInput( externalIdentifier: 'crm-alice-001', email: 'alice@example.org', firstName: 'Alice', lastName: 'Smith', tags: ['Donor', 'VIP'], groups: ['Newsletter', 'Events'], extraFields: ['Wolontariat.volunteer_status' => 'active'], )));
The job implements ShouldBeUnique — duplicate dispatches for the same contact are
de-duplicated at the queue layer (lock key = externalIdentifier or "email:{email}").
Note: The
externalIdentifierpath is implemented as a non-atomicContact.get+ conditionalContact.create/update. A concurrent insert between the get and the create may produce a duplicate. An atomicContact.savewithmatch=['external_identifier']is planned for a future core-lib release.
CreateActivityJob — idempotent activity logger
use CiviCrm\Laravel\Jobs\CreateActivityJob; // Basic — auto-derived dedup key dispatch(new CreateActivityJob( contactId: 42, activityType: 'Phone Call', params: ['subject' => 'Intake call', 'duration' => 15], )); // With an explicit dedupe key for safe at-least-once retries dispatch(new CreateActivityJob( contactId: 42, activityType: 'Phone Call', params: ['subject' => 'Intake call'], dedupeKey: 'form-submission-uuid-abc123', ));
Note: CiviCRM does not deduplicate activities natively. The
ShouldBeUniquelock prevents concurrent duplicates, but if the lock expires before completion a duplicate may be created. Full deduplication via a persistentdedupe_keycolumn is planned for LPR #3 (transactional outbox).
Configuration reference
All options live in config/civicrm.php after publishing. The most important keys:
| Key | Env variable | Default | Description |
|---|---|---|---|
base_url |
CIVICRM_BASE_URL |
null |
CiviCRM APIv4 endpoint URL |
api_token |
CIVICRM_API_TOKEN |
null |
Bearer token / API key |
site_key |
CIVICRM_SITE_KEY |
null |
Optional site key (sent as X-Civi-Key header) |
timeout |
CIVICRM_TIMEOUT |
30 |
Request timeout in seconds (PSR-18 client level) |
verify_tls |
CIVICRM_VERIFY_TLS |
true |
TLS certificate verification |
retry.enabled |
CIVICRM_RETRY |
false |
Exponential-backoff retry (requires core ≥ 0.8) |
queue.connection |
CIVICRM_QUEUE_CONNECTION |
null |
Queue connection for jobs |
queue.queue |
CIVICRM_QUEUE |
default |
Queue name for jobs |
License
MIT — see LICENSE.