Een klant heeft vaak een bestaand systeem waar het leeuwendeel van de business logic in zit. Denk hierbij aan CRM of ERP-software. Dit wordt gebruikt om processen te definiëren, en om grip te krijgen op wat er allemaal in het bedrijf gebeurt. De vraag die vaak naar boven komt bij een opdracht is – “kunnen jullie met dit systeem integreren?”. Integratie is een term waar vrij losjes mee wordt omgesprongen. Wat het daadwerkelijk inhoudt is niet altijd duidelijk. In dit artikel wil ik laten zien wat dit voor een van onze klanten precies inhoudt. Over het algemeen kan een integratie gedefinieerd worden als een stuk software dat twee losse systemen met elkaar laat communiceren. In ons geval is een van die systemen iets wat we zelf nog eerst moeten bouwen.
Voor een van onze klanten hebben we een webapplicatie gebouwd. De applicatie maakt, samengevat, het volgende mogelijk:
- De klanten van de opdrachtgever moeten bestanden kunnen aanleveren.
- Deze bestanden moeten aan bepaalde eisen in een checklist voldoen.
- Om er voor te zorgen dat deze bestanden op tijd worden aangeleverd moeten er tijdig herinneringen worden gestuurd.
Als randvoorwaarde is er gesteld dat de klanten zelf hier geen gebruikersnaam en wachtwoord voor nodig moeten hebben, maar dat de gegevens ook niet open en bloot liggen. De integratie houdt in dat dit systeem met Salesforce communiceert. Om dit voor elkaar te krijgen zijn er diverse onderdelen nodig, waarvan ik de technische kant wil toelichten.
Communiceren met Salesforce
Het eerste wat er mogelijk moet zijn is dat er data opgehaald kan worden uit Salesforce en weer kan worden weggeschreven. Salesforce heeft hiervoor een API. In plaats van direct verzoeken te sturen kunnen we dit iets makkelijker maken met een library zoals https://github.com/omniphx/forrest. Dit is een relatief dunne laag op de API die vaak voorkomende acties net iets makkelijker maakt, maar geen voorwaarden stelt aan hoe deze worden uitgevoerd. Om een verzoek te maken moet er elke keer bewijs (in de vorm van een token) worden geleverd dat aantoont dat wie het verzoek maakt ook toestemming heeft om dit te doen.
De meest voorkomende operatie is om data uit te lezen. De code die hiervoor nodig is, ziet er bijvoorbeeld als volgt uit:
Forrest::authenticate(); $query = " SELECT Id, Name, File_Type__c, Owner__r.Name FROM Client_Files__c WHERE Id = 12345"; $result = Forrest::query($query); return Collection::make($result['records']);
In de documentatie van Forrest wordt er aangeraden om bij elk verzoek eerst te authenticeren – oftewel, het bevestigend antwoord op de vraag “ben jij wie je zegt dat je bent?”. Het resulterende token wordt tijdelijk opgeslagen en vervolgens door het volgende verzoek gebruikt als bewijs dat de aanvrager inderdaad deze vraag mag stellen. Vervolgens wordt er SOQL gebruikt – Salesforce Object Query Language. Dit is domain-specific language die op SQL lijkt (maar het niet is) en die specifiek voor Salesforce bedoeld is. In dit SOQL-statement worden er eigenschappen van een object opgehaald. Verder wordt er van een gerelateerde tabel (Owner) een specifiek veld opgehaald (Name). SOQL gebruikt dus niet zoals in SQL een JOIN-statement, maar benoemt relaties specifiek. Deze relaties moeten ook als zodanig worden opgezet en zijn niet per definitie altijd aanwezig. Je kunt bijvoorbeeld wel vanuit een object de Owner ophalen, maar niet het aantal objecten vanuit de Owner omdat die relatie mist.
Salesforce heeft een aantal standaard-objecten ter beschikking die voor elk bedrijf wel nodig zijn – denk aan een Account (een bedrijf dat een dienst of product afneemt), een Contact (een specifieke persoon binnen zo’n bedrijf), een Lead (een mogelijke verkoop) en meer. Het is ook mogelijk om zelf objecten aan te maken zodat de software beter passend gemaakt kan worden voor het bedrijf. In Salesforce worden deze objecten voorzien van het achtervoegsel __c
– “custom” – om aan te duiden dat ze niet van Salesforce zelf zijn, maar later zijn toegevoegd door de gebruiker. Extra velden worden op dezelfde manier aangegeven; er is standaard een Phone
veld, maar bij situaties waar je altijd 2 telefoonnummers hebt, zou je dus iets als Phone_Secondary__c
kunnen toevoegen.
Don’t Repeat Yourself
Bij het testen van data ophalen werd het al snel duidelijk dat bovenstaande code overal in het systeem nodig was. Verder is het schrijven van een query foutgevoelig; vaak zijn er dezelfde velden nodig die opgehaald moeten worden, en als er per ongeluk een komma te veel staat geeft de API een fout terug. Om dit te voorkomen kan er het Repository-pattern gebruikt worden. Dit is een design pattern – een bepaalde manier om een veel-voorkomend probleem op te lossen – wat het maken van het verzoek abstraheert.
Dit ziet er dan zo uit:
$repository = new ClientFileRepository(); /** @var ClientFileDto $clientFileDto */ $clientFileDto = $repository->getById(12345);
Dit kan dan gelijk verpakt worden in een Data Transfer Object, oftewel DTO. Dit object heeft geen interne logica maar dient puur als container. Omdat hier ook de naamgeving van de eigenschappen van het Salesforce-object aangepast kan worden kunnen eigenschappen betere naamgeving krijgen. Er is immers niet altijd de garantie dat een custom veld een goede naam heeft, en het achtervoegsel __c maakt de code niet leesbaarder.
- De getById-functie heeft dan de volgende taken:
- bouw het correcte SOQL-statement
- authenticeer
- haal de informatie op
- verpak het resultaat in een Collection
- transformeer elk element van de Collection naar een DTO
- geef het eerste element van de Collection terug
Door alle resultaten altijd als een Collection te beschouwen worden de onderliggende delen van de code weer verder geabstraheerd, en dus herbruikbaar.
Fluent Interface
Een repository is ook makkelijk uit te breiden om een set aan resultaten op te halen.
$repository = new ClientFileRepository(); $clientFileDtos = $repository ->getFilesForCampaignId(45678) ->filterByType('pdf');
Een Campaign kan nul of meerdere ClientFiles bevatten. Een ClientFile heeft een bepaald type, waar op verder gefilterd kan worden. Deze techniek wordt fluent interface genoemd. Als er een type bij komt – we hebben nu PDF maar JPG is ook mogelijk – hoeft er geen extra functie geschreven te worden; we hoeven slechts filterByType aan te passen. Dit maakt de code flexibel en aanpasbaar. Het alternatief zou zijn om twee functies te hebben met bijna dezelfde inhoud en bijna dezelfde naam. Dit is foutgevoelig en zorgt voor herhaling.
Aanleveren
In de bovenstaande voorbeelden zijn IDs gebruikt. Hoewel een Salesforce-ID niet numeriek is (gezien het aantal objecten zou dit ook problematisch zijn) is het wel iets waar je naar kunt raden. Voor een ID zoals a1g19000004qCd3AAE
is het mogelijk om een letter of een getal met 1 te verhogen (dus abcd
wordt abce
) om zo een ander ID te raden. Hoewel de kans klein is dat dit gebeurt, is dit wel een risico, en deze informatie mag dus niet zomaar beschikbaar zijn.
De oplossing is encryptie. Hiermee wordt bovenstaand ID versleuteld naar een nieuwe waarde, en deze versleutelde waarde mag de gebruiker inzien. De link naar de upload-pagina bevat dus deze versleutelde versie. Met decryptie kan deze versie door de applicatie weer worden ontcijferd, zodat er naar Salesforce een verzoek gemaakt kan worden om de gegevens op te halen.
Validatie
Bij het aanleveren van bestanden zoals afbeeldingen moet er bijvoorbeeld gecontroleerd worden wat de afmetingen zijn. De informatie hierover is beschikbaar in Salesforce. Het uitvoeren van deze controles eenvoudig; er wordt een dynamische validatie opgebouwd. Validatie in Laravel wordt uitgevoerd met een set instructies.
'image' => ['dimensions:min_width=100,min_height=200']
Door hier de 100 en 200 te vervangen door de waardes die uit Salesforce worden gehaald kan er zo flexibel gecontroleerd worden of de afbeelding voldoet.
Als het bestand wordt geüpload wordt dit naar managed storage gekopieerd. Dit is schaalbare opslag; in plaats van een server waarvan de schijfgrootte van tijd tot tijd moet worden vergroot (wat voor downtime zorgt) is er feitelijk oneindige opslag waar er wordt betaald voor het gebruik. Bij het kopiëren wordt er een URL teruggegeven aan de applicatie, en die URL wordt vervolgens weer terug in Salesforce gezet.
Synchronisatie
Aan het gebruik van een API hangen vaak een aantal voorwaarden. Om er voor te zorgen dat het systeem niet meer wordt belast dan nodig is er een limiet aan hoe vaak de API aangeroepen kan worden; meer verzoeken dan de limiet leiden vaak tot blokkeren. De oplossing hiervoor is om bepaalde verzoeken niet direct via de API uit te voeren, maar eenmalig een lokale kopie te maken van alleen de gegevens die er nodig zijn. Hierna wordt alleen gesynchroniseerd welke informatie er recentelijk veranderd is, wat Salesforce aangeeft met het LastModifiedDate
veld.
We slaan op wanneer de laatste synchronisatie plaats heeft gevonden. Het SOQL-statement wordt dan:
$query = "SELECT Id FROM ClientFiles WHERE LastModifiedDate > $date"
waarbij $date
de datum is waarop de laatste synchronisatie heeft plaatsgevonden. In dit geval is er geen risico op SQL-injection, omdat deze datum niet door de gebruiker kan worden aangepast. Het alleen ophalen van het Id is een keuze om het filteren te scheiden van instantiëren. Omdat deze hoeveelheid van een onbekende grootte kan zijn is het alleen ophalen van het Id altijd sneller dan het ophalen van alle geassocieerde data, en vereist het minder geheugen.
Een verdere optimalisatie is om eerst een telling (count()
) uit te voeren. Als er een groot aantal resultaten zijn (bijvoorbeeld 13958) kan dit aantal opgedeeld worden in 13 brokken van 1000 en 1 van 958. Dit kost dan 14 verzoeken om te maken. Hoewel de meeste verzoeken niet van deze grootte zijn kan het wel voorkomen. Salesforce heeft zelf een limiet van 2000 resultaten per pagina – bij meer wordt er automatisch paginering toegepast vanuit Salesforce zelf. De andere limitatie is het maximaal geheugengebruik van PHP zelf. Om hier optimaler gebruik van te maken wordt er een Iterator toegepast.
Herinneringen
Op het moment dat er herinneringen worden gestuurd wordt de gesynchroniseerde lokale kopie gebruikt om uit te vinden of er wel een herinnering moet worden gestuurd. De te uploaden bestanden hebben een deadline voor het aanleveren. Als deze deadline binnenkort is moet er een herinnering worden gestuurd, maar alleen als er nog niets is aangeleverd, en als de klant de afgelopen vijf werkdagen geen herinnering meer heeft gehad.
Omdat de lokale database sneller is dan Salesforce kost het geen extra moeite om deze controle uit te voeren. Herinneringen worden dus niet slechts eenmalig per dag verstuurd, maar elk half uur. De klant krijgt zo sneller bericht. De lokale opslag betekent ook dat er in Salesforce zelf geen zaken hoeven te worden toegevoegd – er is dus een goede separation of concerns.
Integratie
Een goede integratie opzetten betekent onder andere dat medewerkers in een vertrouwde omgeving kunnen blijven werken terwijl er van buitenaf streng gecontroleerde toegang is. Er hoeft niet – behalve in uitzonderlijke gevallen – tussen applicaties geschakeld te worden. Als Salesforce zelf werd geopend voor de klanten zou dit aanzienlijke licentie- en administratieve kosten met zich meebrengen. Verder is er natuurlijk geen garantie dat de gevraagde functionaliteit beschikbaar is; er zal alsnog een specialistische oplossing gemaakt moeten worden. Het slimme synchronisatieproces zorgt er voor dat er geen zware server nodig is. Mocht er om welke reden dan ook toch schaalvergroting nodig zijn, dan is dankzij Docker een snelle verhuizing mogelijk naar een andere machine.