Created
June 20, 2014 14:05
-
-
Save afawcett/2545650dc851eb0552e6 to your computer and use it in GitHub Desktop.
Preview of FLS Support in Domain Layer (see comments below)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public with sharing class Opportunities extends fflib_SObjectDomain | |
{ | |
public Opportunities(List<Opportunity> sObjectList) | |
{ | |
// Domain classes are initialised with lists to enforce bulkification throughout | |
super(sObjectList); | |
} | |
public override void onApplyDefaults() | |
{ | |
// Apply defaults to Opportunities | |
for(Opportunity opportunity : (List<Opportunity>) Records) | |
{ | |
opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c; | |
} | |
} | |
public override void onValidate() | |
{ | |
// Validate Opportunities | |
for(Opportunity opp : (List<Opportunity>) Records) | |
{ | |
if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId == null) | |
{ | |
opp.AccountId.addError( error('You must provide an Account for Opportunities for existing Customers.', opp, Opportunity.AccountId) ); | |
} | |
} | |
} | |
public override void onValidate(Map<Id,SObject> existingRecords) | |
{ | |
// Validate changes to Opportunities | |
for(Opportunity opp : (List<Opportunity>) Records) | |
{ | |
Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id); | |
if(opp.Type != existingOpp.Type) | |
{ | |
opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) ); | |
} | |
} | |
} | |
public override void onAfterInsert() | |
{ | |
// Unit of Work scope for this event | |
fflib_SObjectUnitOfWork uow = | |
new fflib_SObjectUnitOfWork(new Schema.SObjectType[] { Account.SObjectType }); | |
// Update last Opportunity activity on the related Accounts (via the Accounts Domain class) | |
Accounts accounts = new Accounts( | |
new AccountsSelector().selectByOpportunity(Records)); | |
accounts.updateOpportunityActivity(uow); | |
// Commit the work | |
uow.commitWork(); | |
} | |
public override void onAfterDelete() | |
{ | |
for(Opportunity opp : (List<Opportunity>) Records) | |
{ | |
if(opp.StageName!=null && opp.StageName.startsWith('Won')) | |
{ | |
opp.StageName.addError( error('You cannot delete Won Opportunities.', opp, Opportunity.StageName) ); | |
} | |
} | |
} | |
public void applyDiscount(Decimal discountPercentage, fflib_SObjectUnitOfWork uow) | |
{ | |
// Custom FLS checking | |
checkFieldIsUpdateable(Opportunity.StageName); | |
// Calculate discount factor | |
Decimal factor = calculateDiscountFactor(discountPercentage); | |
// Opportunity lines to apply discount to | |
List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>(); | |
// Apply discount | |
for(Opportunity opportunity : (List<Opportunity>) Records) | |
{ | |
// Appply to the Opporunity Amount? | |
if(opportunity.OpportunityLineItems.size()==0) | |
{ | |
// Adjust the Amount on the Opportunity if no lines | |
opportunity.Amount = opportunity.Amount * factor; | |
uow.registerDirty(opportunity); | |
} | |
else | |
{ | |
// Collect lines to apply discount to | |
linesToApplyDiscount.addAll(opportunity.OpportunityLineItems); | |
} | |
} | |
// Apply discount to lines | |
OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount); | |
lineItems.applyDiscount(this, discountPercentage, uow); | |
} | |
public static Decimal calculateDiscountFactor(Decimal discountPercentage) | |
{ | |
// Calculate discount factor | |
Decimal discountProportion = discountPercentage==null ? 0 : discountPercentage / 100; | |
Decimal factor = 1 - discountProportion; | |
return factor; | |
} | |
public class Constructor implements fflib_SObjectDomain.IConstructable, fflib_SObjectDomain.IConfigure | |
{ | |
public fflib_SObjectDomain construct(List<SObject> sObjectList) | |
{ | |
return new Opportunities(sObjectList); | |
} | |
public fflib_SObjectDomain.Configuration configure() | |
{ | |
return new fflib_SObjectDomain.Configuration(). | |
setEnforceCRUD(true). | |
setEnforceTriggerFLS(true). | |
onTriggerEnforceFLSRead(Opportunity.Type). | |
onTriggerEnforceFLSRead(Opportunity.StageName). | |
onTriggerEnforceFLSReadWrite(Opportunity.DiscountType__c); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
That's a good question, i've seen lots of examples of triggers being forced to do to much business logic (utilising a pattern i call 'control fields') and getting into a big mess with recursive calls, data loaders etc.
So without wishing to broaden this into a wider debate of trigger vs service logic. The answer in my view is, we do need logic in the triggers to protect the data integrity and provide some feature function around default fields and manipulating other records (subject to how users interact with the app). Where one draws the line is hard to prescribe though (certainly callouts and batch apex are troublesome).
It also somewhat depends on how your users want to interact with your app via CRUD Native UI and/or via VF/Custom UI, really you have to consider both tbh. One thing is for certain, we have to think carefully before blocking direct object access or not writing approprite trigger code as it may erode platform features like Workflow, Flow and not to mention Salesforce1 Mobile.
In respect to your other quesiton, also a good one. If the developer configures triggerEnforceFLS without giving any fields, i'm tempted to throw an assertion at runtime, to say, something along the lines of 'why ask me to enforce something you've not told me about'. We could of course implicitly enable it the first time onTriggerEnforceFLSRead or onTriggerEnforceFLSReadWrite is called?. It's also present to allow disabling.
New question....