PHPackages                             woduda/civicrm-laravel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. woduda/civicrm-laravel

ActiveLibrary[API Development](/categories/api)

woduda/civicrm-laravel
======================

Thin Laravel adapter around woduda/civicrm-php (PSR-18 CiviCRM APIv4 client)

v0.2.0(yesterday)02↑2900%MITPHPPHP ^8.3CI passing

Since Jun 8Pushed yesterdayCompare

[ Source](https://github.com/woduda/civicrm-laravel)[ Packagist](https://packagist.org/packages/woduda/civicrm-laravel)[ RSS](/packages/woduda-civicrm-laravel/feed)WikiDiscussions main Synced yesterday

READMEChangelog (2)Dependencies (14)Versions (3)Used By (0)

civicrm-laravel
===============

[](#civicrm-laravel)

Thin, idiomatic Laravel adapter around [`woduda/civicrm-php`](https://github.com/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
----------

[](#quickstart)

### 1. Install

[](#1-install)

```
composer require woduda/civicrm-laravel
```

Laravel's package auto-discovery registers the service provider and `CiviCrm` facade automatically.

### 2. Configure `.env`

[](#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

[](#3-verify-the-connection)

```
php artisan civicrm:test-connection
# OK  https://your-site.example.org/civicrm/ajax/api4/  (42 ms)
```

### 4. Use the facade

[](#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
-----------

[](#queued-jobs)

### SyncContactJob — idempotent contact upsert

[](#synccontactjob--idempotent-contact-upsert)

Dispatching the job enqueues an upsert that:

1. Looks up the contact by `externalIdentifier` (if provided) or by email via `upsertByEmail`
2. Creates or updates the contact with the provided fields
3. 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 `externalIdentifier` path is implemented as a non-atomic `Contact.get` + conditional `Contact.create/update`. A concurrent insert between the get and the create may produce a duplicate. An atomic `Contact.save` with `match=['external_identifier']` is planned for a future core-lib release.

### CreateActivityJob — idempotent activity logger

[](#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 `ShouldBeUnique`lock prevents concurrent duplicates, but if the lock expires before completion a duplicate may be created. Full deduplication via a persistent `dedupe_key` column is planned for LPR #3 (transactional outbox).

Transactional Outbox
--------------------

[](#transactional-outbox)

The outbox guarantees that CiviCRM side-effects are persisted atomically alongside domain model changes — no distributed transaction required. A drain command picks up and executes pending entries.

### 1. Enable and publish the migration

[](#1-enable-and-publish-the-migration)

In `.env` (or `config/civicrm.php`):

```
CIVICRM_OUTBOX=true
```

Publish and run the migration:

```
php artisan vendor:publish --tag=civicrm-laravel-migrations
php artisan migrate
```

### 2. Write to the outbox inside a DB transaction

[](#2-write-to-the-outbox-inside-a-db-transaction)

```
use CiviCrm\Laravel\Data\ContactInput;
use CiviCrm\Laravel\Outbox\OutboxRepository;
use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($outbox, $contactInput): void {
    // Save your domain model here …
    $domain->save();

    // … then enqueue the CiviCRM side-effect in the same transaction.
    $outbox->pushSyncContact($contactInput);
});
```

If the transaction rolls back the outbox entry is discarded with it. If it commits, `civicrm:outbox:work` will pick it up.

### 3. Register the drain command in the scheduler

[](#3-register-the-drain-command-in-the-scheduler)

```
// routes/console.php or App\Console\Kernel::schedule()
$schedule->command('civicrm:outbox:work')->everyMinute();
```

The command is idempotent and safe to overlap — row-level locking in `reserveBatch()` prevents duplicate processing.

### Available push helpers

[](#available-push-helpers)

MethodDescription`pushSyncContact(ContactInput $input)`Enqueues a contact upsert; dedupe key derived from `externalIdentifier` or `email``pushCreateActivity(int $contactId, string $type, array $params, ?string $dedupeKey)`Enqueues an activity; pass an explicit `$dedupeKey` for at-least-once safety`push(string $type, array $payload, ?string $dedupeKey)`Raw push for custom entry types### Drain command options

[](#drain-command-options)

```
php artisan civicrm:outbox:work [--limit=100] [--max-attempts=5]

```

- `--limit` — maximum entries processed per run (default 100)
- `--max-attempts` — attempts before permanent failure (default 5); exponential backoff capped at 3600 s

`ValidationException` and `AuthenticationException` (when available) cause immediate permanent failure without retry.

Configuration reference
-----------------------

[](#configuration-reference)

All options live in `config/civicrm.php` after publishing. The most important keys:

KeyEnv variableDefaultDescription`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 jobsLicense
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/0b6d932b8d01170831cec6c38053d345bf16083bba2c38e98c3fc460f0bba757?d=identicon)[woduda](/maintainers/woduda)

---

Top Contributors

[![woduda](https://avatars.githubusercontent.com/u/1030651?v=4)](https://github.com/woduda "woduda (11 commits)")

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/woduda-civicrm-laravel/health.svg)

```
[![Health](https://phpackages.com/badges/woduda-civicrm-laravel/health.svg)](https://phpackages.com/packages/woduda-civicrm-laravel)
```

###  Alternatives

[laravel/sail

Docker files for running a basic Laravel application.

1.9k199.2M1.2k](/packages/laravel-sail)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

815320.5k3](/packages/defstudio-telegraph)[spatie/laravel-health

Monitor the health of a Laravel application

88011.3M149](/packages/spatie-laravel-health)[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.1k9.9M87](/packages/dedoc-scramble)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[simplestats-io/laravel-client

Analytics for Laravel. Track visitors, registrations, and payments. Discover which channels actually drive revenue, not just traffic. Server-side, GDPR compliant, ad-blocker proof.

5019.3k](/packages/simplestats-io-laravel-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
