# Laravel

Source: https://docs.mirafive.io/sdks/laravel

> Track MIRA FIVE events from Laravel, evaluate feature flags in-process and add the tracker tag with one Blade directive.

`mirafive/sdk-laravel` adds MIRA FIVE to a Laravel app: a `Mira` facade for server events and feature flags, Blade directives for the tracker tag and the flag bootstrap, and `Mira::fake()` for tests. Events are sent after the response, so pages never wait for analytics. It wraps the [PHP SDK](https://docs.mirafive.io/sdks/php).

## Install

```sh
composer require mirafive/sdk-laravel
```

Requires PHP 8.3 or newer and Laravel 11, 12 or 13. The service provider and the `Mira` facade are discovered automatically. No configuration file is needed; to change the defaults, publish it:

```sh
php artisan vendor:publish --tag=mirafive-config
```

## Set up

You need two keys, see [Keys](https://docs.mirafive.io/keys) for where to find them: the **secret key** of a server source for server events and flags, and the **website key** of a website source for the tracker tag. Put both in `.env`:

```sh title=".env"
MIRAFIVE_SECRET_KEY=mf_…
MIRAFIVE_WEBSITE_KEY=mf_…
```

Add the tracker to your layout's `<head>`:

```blade title="resources/views/layouts/app.blade.php"
<head>
    @mirafiveScript
</head>
```

It prints the hosted script with your website key, and a Vite CSP nonce when one is set. The secret key never reaches the page. The tracker starts in [consentless mode](https://docs.mirafive.io/guides/consent#consentless-mode): no cookies, no storage, no consent banner.

Track a server event where it happens:

```php title="app/Http/Controllers/SignupController.php"
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use MiraFive\Laravel\Facades\Mira;

class SignupController extends Controller
{
    public function store(Request $request): RedirectResponse
    {
        $user = User::create($request->validate([
            'name' => ['required', 'string'],
            'email' => ['required', 'email'],
            'password' => ['required', 'min:12'],
        ]));

        Mira::track('signup', userId: (string) $user->id, properties: ['plan' => 'free']);

        return redirect()->route('dashboard');
    }
}
```

That is the whole install. `track()` buffers, and the package sends the buffer after the response. You never call `flush()`.

Server events are sent in [full mode](https://docs.mirafive.io/guides/consent#full-mode) by default. See [Consent](#consent).

## Verify

1. Run the check command. It sends an install check, which is never stored or billed, and prints the receipt:

   ```sh
   php artisan mirafive:check
   ```

   It ends with `The key and host work.` A wrong key prints the error code instead, such as `unauthorized`.
2. Load a page on its real domain and look for `POST https://events.mirafive.io/v1/batch/mf_…` answering `202` in the browser's network tab. The tracker skips `localhost` unless you add `data-track-localhost` (see [Tracker tag](#tracker-tag)).
3. Trigger a server event, then open the source's live view in MIRA FIVE and find both.

Nothing arriving? See [Troubleshooting](#troubleshooting).

## Track events

The facade takes the same named arguments as the [PHP SDK](https://docs.mirafive.io/sdks/php#track-events):

```php
use MiraFive\Laravel\Facades\Mira;

Mira::track('order completed', userId: (string) $order->user_id, properties: [
    'revenue' => $order->total,
    'currency' => 'EUR',
]);
```

`Mira::forUser($user)` fills in `getAuthIdentifier()` as the user id, which is the primary key for Eloquent users:

```php
Mira::forUser($request->user())->track('report_exported', ['format' => 'csv']);
```

For events that must be recorded exactly once, such as a payment webhook, `Mira::send()` sends at once with an idempotency key and returns the `MiraFive\Receipt`. It never goes through the queue:

```php
Mira::send([
    ['name' => 'order completed', 'userId' => (string) $order->user_id, 'properties' => ['revenue' => 129, 'currency' => 'EUR']],
], idempotencyKey: "order-{$order->id}");
```

`MiraFive\Mira` and `MiraFive\Flags\MiraFlags` are container singletons; you can inject them instead of using the facade. Input MIRA FIVE would refuse throws an `InvalidArgumentException` at the call. Event names, properties and revenue are covered in [Track events](https://docs.mirafive.io/guides/track-events).

## Identify users

Identify the user after login, with the internal id and a few traits. In `app/Providers/AppServiceProvider.php`:

```php
use Illuminate\Auth\Events\Login;
use Illuminate\Support\Facades\Event;
use MiraFive\Laravel\Facades\Mira;

public function boot(): void
{
    Event::listen(Login::class, function (Login $event): void {
        Mira::forUser($event->user)->identify(['plan' => $event->user->plan]);
    });
}
```

Never send an email address as the user id. See [Identify users](https://docs.mirafive.io/guides/identify-users).

## Tracker tag

`@mirafiveScript` takes more tracker attributes as an array. `true` prints a bare attribute:

```blade
@mirafiveScript(['data-autocapture' => true, 'data-site-search' => 'q'])
```

| Setting | What it prints |
| --- | --- |
| `MIRAFIVE_WEBSITE_KEY` | `data-key`. Without it, the directive prints nothing. |
| `MIRAFIVE_SCRIPT_MODE=full` | `data-mode="full"`. |
| `MIRAFIVE_HOST` other than the default | `data-host`. |
| `MIRAFIVE_SCRIPT_URL` | `src`, e.g. a self-hosted copy. Default `https://cdn.mirafive.io/mira.js`. |
| `'nonce' => '…'` in the array | A CSP nonce on both script tags, instead of Vite's. |

The attributes the tracker reads are listed on the [script tag page](https://docs.mirafive.io/sdks/script-tag).

## Consent

Server events and the tracker have separate collection modes:

| | Setting | Default |
| --- | --- | --- |
| Server events | `MIRAFIVE_MODE` | `full` |
| Tracker tag | `MIRAFIVE_SCRIPT_MODE` | `consentless` |

- **Full** server events carry the `userId`, `anonymousId` and `sessionId` you pass. Your site holds the consent or other lawful basis for them; the package cannot ask.
- **Consentless** carries no identifiers. On the server, passing one throws an `InvalidArgumentException` on the first call.
- A tracker in full mode stores nothing until your consent banner grants it with `mirafive('consent', true)`. See [Script tag](https://docs.mirafive.io/sdks/script-tag).
- No personal data in event names or properties: no names, email addresses, phone numbers or free text a person typed.

See [Consent](https://docs.mirafive.io/guides/consent).

## Feature flags

`Mira::flags()` is the `MiraFive\Flags\MiraFlags` of your server source. Its answers come from a flag document it fetches on first use and shares between requests through your cache store:

```php
use MiraFive\Laravel\Facades\Mira;

$flags = Mira::flags()->for(
    userId: (string) $request->user()->id,
    properties: ['plan' => $request->user()->plan],
    consent: ['experiments' => true, 'targeting' => false],
    optedOut: Mira::optedOut($request),
);

$flags->enabled('new-checkout');                     // bool
$flags->variant('pricing-test', 'control');          // string
$flags->config('checkout-limits', ['maxItems' => 10]); // the variant's value
```

`Mira::optedOut($request)` is `true` when the request carries `Sec-GPC: 1` or `DNT: 1`. `Mira::forUser($user)->flags(properties: [...])` does the same with the user id filled in, and reads the opt-out from the current request unless you pass `optedOut`.

Reads never throw; without a document every flag answers its fallback. The unit, consent, segments and experiments work as in the [PHP SDK](https://docs.mirafive.io/sdks/php#feature-flags). See [Feature flags](https://docs.mirafive.io/guides/feature-flags).

### Bootstrap in Blade

Hand the server's answers to the browser so the first paint shows the right variant:

```blade
<head>
    @mirafiveFlags(['userId' => auth()->id(), 'properties' => ['plan' => auth()->user()?->plan]])
    @mirafiveScript(['data-flags' => true])
</head>
```

The array takes `userId`, `anonymousId`, `properties`, `consent` and `optedOut`; `optedOut` is read from `Sec-GPC` and `DNT` when you leave it out. You can also pass flags you already read: `@mirafiveFlags($flags)`. The block carries only flags your website reads, escaped so no value can end the script.

A page with a bootstrap belongs to one visitor. The package adds `Cache-Control: private, no-store` to every response whose view printed `@mirafiveFlags`. If you print `$flags->bootstrap()` yourself, send the header too:

```php
use MiraFive\Flags\MiraFlags;

return response($html)->withHeaders(MiraFlags::BOOTSTRAP_HEADERS);
```

## Queues and Octane

**When events are sent.** The buffer is sent once when Laravel terminates (after the response under PHP-FPM), after each Octane request, task and tick, and after each queued job. The PHP SDK's own shutdown flush is off. Every 100 events it is sent early. `send()` is always immediate.

**Queued delivery.** Set `MIRAFIVE_QUEUE` and each buffered batch is encoded once and handed to a `MiraFive\Laravel\Queue\SendBatch` job, so the request does no delivery work:

```sh title=".env"
MIRAFIVE_QUEUE=redis:mirafive
```

The value is a queue name (`mirafive`), `connection:queue` (`redis:mirafive`), or `true` for the default queue. Run a worker for it:

```sh
php artisan queue:work redis --queue=mirafive
```

- The job carries the body, never the key: the worker sends it with its own configuration.
- The body keeps its batch id, so a job that runs twice is stored once.
- Retryable failures are retried by the queue: 5 tries, backoff of 10 s, 1 min, 5 min and 15 min. Refused batches are logged and dropped.
- A worker without `MIRAFIVE_SECRET_KEY` logs an error and fails the job instead of dropping it. A worker with `MIRAFIVE_ENABLED=false` drops it.
- Batches are up to 1 MiB of JSON. Check your queue's message limit (SQS allows 256 KB).

**Octane.** No request state is kept in singletons, and the buffer is flushed at the end of every request. The package adds `MiraFive\Mira`, `MiraFlags`, the transport and the facade's client to `octane.warm`, so a worker keeps one HTTP connection and one flag document.

## Testing

`Mira::fake()` records instead of sending:

```php title="tests/Feature/SignupTest.php"
use MiraFive\Laravel\Facades\Mira;

it('tracks the signup', function () {
    $mira = Mira::fake();

    $this->post('/signup', [
        'name' => 'Ada',
        'email' => 'ada@example.com',
        'password' => 'correct-horse-battery',
    ])->assertRedirect();

    $mira->assertTracked('signup');
    $mira->assertTracked('signup', fn (array $event) => $event['properties']['plan'] === 'free');
    $mira->assertTracked('signup', 1); // exactly once
    $mira->assertNotTracked('refund');
});

it('stays quiet', function () {
    Mira::fake()->assertNothingTracked();
});
```

The fake records `track()`, `identify()` and `send()` through the facade and through an injected `MiraFive\Laravel\Client`. Input is still checked, so an event MIRA FIVE would refuse fails the test. Flags answer their fallbacks. Code that injects `MiraFive\Mira` directly is not faked: set `MIRAFIVE_ENABLED=false` in `phpunit.xml` so it sends nothing:

```xml title="phpunit.xml"
<env name="MIRAFIVE_ENABLED" value="false"/>
```

## API reference

### Configuration

`config/mirafive.php`, publish tag `mirafive-config`.

| Key | Env | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `MIRAFIVE_ENABLED` | `true` | Off, or without a secret key: nothing is sent and no key is needed, flags answer their fallbacks. Off also omits the tracker tag. |
| `secret_key` | `MIRAFIVE_SECRET_KEY` | | Secret key of a server source. |
| `website_key` | `MIRAFIVE_WEBSITE_KEY` | | Website key, printed by `@mirafiveScript`. |
| `host` | `MIRAFIVE_HOST` | `https://events.mirafive.io` | With scheme. |
| `mode` | `MIRAFIVE_MODE` | `full` | Server events: `full` or `consentless`. |
| `script_mode` | `MIRAFIVE_SCRIPT_MODE` | `consentless` | The tracker tag: `consentless` or `full`. |
| `queue` | `MIRAFIVE_QUEUE` | `null` | `null` sends after the response; a queue name or `connection:queue` hands each batch to a job. |
| `script_url` | `MIRAFIVE_SCRIPT_URL` | `https://cdn.mirafive.io/mira.js` | The tracker script. |
| `flags.refresh_seconds` | `MIRAFIVE_FLAGS_REFRESH` | `30` | How old the flag document may get. At least 10. |
| `flags.cache_store` | `MIRAFIVE_FLAGS_CACHE` | default store | The cache store that shares the flag document and the delivery pause between requests. |

Delivery failures go to your default log channel as warnings, prefixed `[mirafive]`. To send through your own HTTP client, bind `MiraFive\Http\Transport` (for example to `MiraFive\Http\Psr18Transport`) before the client is resolved.

### `MiraFive\Laravel\Facades\Mira`

| Method | Description |
| --- | --- |
| `track(string $name, ?string $userId = null, ?string $anonymousId = null, ?string $sessionId = null, array $properties = [], DateTimeInterface\|int\|null $time = null, ?array $page = null): void` | Buffers one event. |
| `identify(string $userId, array $traits = [], ?string $anonymousId = null, DateTimeInterface\|int\|null $time = null): void` | Buffers `$identify`. |
| `send(array $events, ?string $idempotencyKey = null): Receipt` | Sends now. Throws `MiraFive\MiraError`. |
| `flush(): void` | Sends the buffer. The package calls it for you. |
| `flags(): MiraFlags` | The flags of the server source. |
| `forUser(Authenticatable $user): UserContext` | The calls below with `getAuthIdentifier()` as the user id. |
| `optedOut(Request $request): bool` | Whether the request carries `Sec-GPC: 1` or `DNT: 1`. |
| `fake(): MiraFake` | Swaps the facade for a recorder. |

`UserContext` (from `forUser()`):

| Method | Description |
| --- | --- |
| `track(string $name, array $properties = [], ?string $anonymousId = null, ?string $sessionId = null, DateTimeInterface\|int\|null $time = null, ?array $page = null): void` | Buffers one event for the user. |
| `identify(array $traits = [], ?string $anonymousId = null): void` | Buffers `$identify` for the user. |
| `flags(array $properties = [], array $consent = [], ?string $anonymousId = null, ?bool $optedOut = null): UserFlags` | The user's flags. `null` reads the opt-out from the current request. |

### Blade

| Directive | Description |
| --- | --- |
| `@mirafiveScript` or `@mirafiveScript([...])` | The tracker tag with the website key and extra attributes. |
| `@mirafiveFlags([...])` or `@mirafiveFlags($flags)` | The flag bootstrap block. Adds `Cache-Control: private, no-store` to the response. |

### `MiraFive\Laravel\Testing\MiraFake`

| Method | Description |
| --- | --- |
| `assertTracked(string $name, callable\|int\|null $callback = null)` | Tracked at least once, matching the callback, or exactly this many times. The callback receives `name`, `userId`, `anonymousId`, `sessionId`, `properties`, `time`, `page`. |
| `assertNotTracked(string $name, ?callable $callback = null)` | Not tracked (matching the callback). |
| `assertIdentified(?string $userId = null, ?callable $callback = null)` | A user (this user) was identified. The callback receives `userId`, `traits`, `anonymousId`. |
| `assertNothingTracked()` | Nothing tracked, sent or identified. |
| `tracked(?string $name = null)`, `identified()` | What was recorded. |

### Artisan

`php artisan mirafive:check` sends `$install_check` with the configured key and host and prints the batch, the accepted and dropped counts and the reason. It fails unless the reason is `install_check`.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| Nothing arrives | Run `php artisan mirafive:check`, then look for `[mirafive]` warnings in your log. |
| `unauthorized` | `MIRAFIVE_SECRET_KEY` is missing, wrong or revoked; it must be the secret key of a server source. After changing `.env` in production, run `php artisan config:cache` again. |
| `website_key_as_bearer` | The website key ended up in `MIRAFIVE_SECRET_KEY`. Server code needs the secret key. |
| `InvalidArgumentException: A consentless client may not send userId` | `MIRAFIVE_MODE` is `consentless`. Drop the identifiers, or switch to `full` where you hold consent. |
| No tracker tag | `MIRAFIVE_WEBSITE_KEY` is empty or `MIRAFIVE_ENABLED` is false. |
| Queued batches never arrive | No worker runs the configured queue, e.g. `php artisan queue:work redis --queue=mirafive`. |
| A flag always answers its fallback | Check `Mira::flags()->status()` and `$flags->evaluate($key)->errorCode`: `NOT_READY` means no document (see the log), `FLAG_NOT_FOUND` that the flag is not a flag of this source. |

## Set up with an AI agent

Paste this into your coding agent:

```text
Add MIRA FIVE analytics to this Laravel application with the Composer package mirafive/sdk-laravel.
Docs: https://docs.mirafive.io/sdks/laravel.md

1. Run `composer require mirafive/sdk-laravel` (PHP 8.3+, Laravel 11–13). The provider and the Mira facade are
   auto-discovered; do not register them by hand.
2. Add to .env.example (empty values) and ask me for the real values for .env:
     MIRAFIVE_SECRET_KEY=   (secret key of a server source; never print it into HTML or JavaScript)
     MIRAFIVE_WEBSITE_KEY=  (website key of a website source)
   Only add MIRAFIVE_HOST, MIRAFIVE_MODE or MIRAFIVE_SCRIPT_MODE if I ask for them. The tracker is consentless by
   default; set MIRAFIVE_SCRIPT_MODE=full only if the site has a consent banner that calls mirafive('consent', …).
3. Put @mirafiveScript inside <head> of the main Blade layout(s), once per page.
4. Track the signup where the user is created:
     \MiraFive\Laravel\Facades\Mira::track('signup', userId: (string) $user->getAuthIdentifier(), properties: ['plan' => $plan]);
   Use the internal user id, never an email address. No personal data in event names or properties.
   Identify on login: listen to Illuminate\Auth\Events\Login and call
     Mira::forUser($event->user)->identify(['plan' => ...]);
5. Do not call flush(); the package sends after the response, after Octane requests and after queued jobs.
6. In tests, call $mira = Mira::fake(); and assert with $mira->assertTracked('signup').
   Add <env name="MIRAFIVE_ENABLED" value="false"/> to phpunit.xml.
7. Verify with `php artisan mirafive:check`: it must end with "The key and host work". Report what you changed.
Do not add other analytics libraries, cookies or consent banners.
```
