iltuostudiodigitale / crm-connector
Contratto e client per inviare i contatti raccolti dai form al CRM del gestionale.
Requires
- php: ^8.3
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- spatie/laravel-data: ^4.15
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0|^4.0
README
Contratto e client per inviare al CRM del gestionale i contatti raccolti dai form di un sito.
Il pacchetto esiste per un motivo solo: il sito non decide il formato. La submission si costruisce
passando da CrmContactData; se i dati non rispettano il contratto, la richiesta non parte.
Installazione
composer require iltuostudiodigitale/crm-connector php artisan crm-connector:install
Il comando pubblica la configurazione e deposita le istruzioni per gli agenti AI
(AGENTS.md) dentro il progetto — in .ai/rules/crm-connector.md se il progetto usa
quella convenzione, altrimenti nella root. Dentro vendor/ nessun agente le leggerebbe.
Credenziali
Le rilascia il gestionale in Amministrazione → CRM → Siti, con l'azione «Credenziali». Il client secret è visibile solo al momento della generazione: se lo si perde va rigenerato, e i token già emessi smettono di funzionare.
CRM_BASE_URL=https://gestionale.esempio.it CRM_SUBMISSION_URL=https://crm.esempio.it/api/v1/form-submission CRM_CLIENT_ID=9f8e... CRM_CLIENT_SECRET=...
Il secret va tenuto lato server. Se il form invia da JavaScript nel browser, la POST deve comunque passare da un controller del sito: il secret non va mai messo in una pagina.
Uso
use IlTuoStudioDigitale\CrmConnector\Data\CrmContactData; use IlTuoStudioDigitale\CrmConnector\Facades\Crm; $contact = CrmContactData::validateAndCreate([ 'form_name' => 'Contattaci', 'first_name' => $request->input('nome'), 'last_name' => $request->input('cognome'), 'email' => $request->input('email'), 'phone' => $request->input('telefono'), 'message' => $request->input('messaggio'), 'privacy_accepted' => $request->boolean('privacy'), 'source_url' => $request->fullUrl(), ]); $result = Crm::submit($contact); // ['contact_id' => 123, 'created_at' => '...']
In caso di errore viene sollevata CrmSubmissionFailedException, con ->status e ->errors
(la mappa di validazione restituita dal gestionale).
Versioni
Il major cambia a ogni rottura del contratto: un campo che sparisce, uno che diventa obbligatorio, una regola più stretta. Mai come minor.
L'URL dell'API (/api/v1) cambia invece solo quando il gestionale deve servire due
contratti contemporaneamente, perché ci sono siti in produzione fermi alla versione
precedente. Finché tutti i siti installati parlano lo stesso contratto, una major del
pacchetto non comporta una nuova versione dell'API.
Il contratto
| Campo | Obbligatorio | Regole |
|---|---|---|
first_name |
sì | max 150 |
last_name |
sì | max 150 |
privacy_accepted |
sì | deve essere true |
email |
se manca phone |
email valida, max 255 |
phone |
se manca email |
max 20 |
business_name |
no | max 150 |
fiscal_code |
no | 11–16 caratteri alfanumerici |
message |
no | testo libero |
newsletter_accepted |
no | default false |
source_url |
no | URL valido |
referrer_url |
no | URL valido |
form_name |
sì | max 150 — il nome del modulo che ha prodotto il contatto |
custom |
no | mappa di risposte libere: max 200 chiavi, ogni valore max 5000 |
form_name: da dove arriva il dato
È obbligatorio su ogni invio, anche per un semplice modulo di contatto, perché è la chiave con cui il gestionale raggruppa quello che riceve: senza, tutte le richieste di tutti i moduli di un sito finiscono in un unico mucchio indistinto.
Il modo consigliato è un campo nascosto nel modulo, così il nome viaggia insieme ai dati e non va ricordato lato server:
<input type="hidden" name="form_name" value="Questionario mutuo 2026">
'form_name' => $request->input('form_name', 'Contattaci'),
Due accortezze:
- Usa lo stesso nome, scritto sempre allo stesso modo. Non esiste un elenco dei moduli nel gestionale: il nome è l'unica cosa che li distingue, quindi «Questionario mutuo» e «Questionario mutui» diventano due moduli diversi nei filtri e nei conteggi.
- Cambia nome quando cambia il modulo in modo incompatibile (per esempio riscrivi le domande di un questionario): così lo storico resta leggibile invece di mescolare risposte a domande diverse sotto la stessa etichetta.
Campi custom e questionari
I campi fissi sopra sono il minimo che ogni form deve avere. Tutto il resto — un
questionario di quaranta domande, un preventivatore, una richiesta con campi specifici
del sito — va in custom, che accetta qualunque mappa chiave/valore: stringhe, numeri,
booleani, null e liste di scalari.
CrmContactData::validateAndCreate([ 'first_name' => 'Mario', 'last_name' => 'Rossi', 'email' => 'mario@example.com', 'privacy_accepted' => $request->boolean('privacy'), 'form_name' => 'Questionario mutuo 2026', 'custom' => [ 'reddito_annuo' => 32000, 'tipo_immobile' => 'prima_casa', 'ha_mutui_in_corso' => false, 'province_interesse' => ['NA', 'SA'], 'note_libere' => null, ], ]);
Dentro custom un null è una risposta lasciata in bianco, ed è diverso da una
domanda mai posta: per questo custom viene inviato così com'è, senza ripulire i valori
vuoti. I limiti (200 chiavi, 5000 caratteri per valore) esistono per non trasformare
l'endpoint in un deposito.
Non sono inviabili: il sito di provenienza (deriva dalle credenziali), l'indirizzo IP e lo user agent (li rileva il gestionale), le note interne.
Siti non-Laravel
Il pacchetto è una comodità, non un obbligo tecnico: il contratto è HTTP. Due chiamate:
# 1. token (client credentials) curl -X POST https://gestionale.esempio.it/oauth/token \ -d grant_type=client_credentials \ -d client_id=$CRM_CLIENT_ID \ -d client_secret=$CRM_CLIENT_SECRET \ -d scope=crm:submit # 2. submission curl -X POST https://crm.esempio.it/api/v1/form-submission \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"form_name":"Contattaci","first_name":"Mario","last_name":"Rossi","email":"mario@example.com","privacy_accepted":true}'
Risposte: 201 con {"success":true,"data":{"contact_id":…}}, 422 con errors,
401 token assente o scaduto, 403 sito non attivo o origine non consentita, 429 rate limit.