Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1 @@
|
||||
[{"contact":{"id":38186,"points":0,"color":null,"fields":{"core":{"points":{"id":"9","label":"Points","alias":"points","type":"number","group":"core","object":"lead","is_fixed":"1","value":"0"},"title":{"id":"1","label":"Title","alias":"title","type":"lookup","group":"core","object":"lead","is_fixed":"1","value":""},"firstname":{"id":"2","label":"First Name","alias":"firstname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Test"},"lastname":{"id":"3","label":"Last Name","alias":"lastname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Contact"},"company":{"id":"4","label":"Company","alias":"company","type":"text","group":"core","object":"lead","is_fixed":"1","value":""},"position":{"id":"5","label":"Position","alias":"position","type":"text","group":"core","object":"lead","is_fixed":"1","value":""},"email":{"id":"6","label":"Email","alias":"email","type":"email","group":"core","object":"lead","is_fixed":"1","value":"Test@email.com"},"phone":{"id":"8","label":"Phone","alias":"phone","type":"tel","group":"core","object":"lead","is_fixed":"1","value":""},"mobile":{"id":"7","label":"Mobile","alias":"mobile","type":"tel","group":"core","object":"lead","is_fixed":"1","value":""},"address1":{"id":"11","label":"Address Line 1","alias":"address1","type":"text","group":"core","object":"lead","is_fixed":"1","value":""},"address2":{"id":"12","label":"Address Line 2","alias":"address2","type":"text","group":"core","object":"lead","is_fixed":"1","value":""},"city":{"id":"13","label":"City","alias":"city","type":"text","group":"core","object":"lead","is_fixed":"1","value":""},"state":{"id":"14","label":"State","alias":"state","type":"region","group":"core","object":"lead","is_fixed":"1","value":""},"zipcode":{"id":"15","label":"Zip Code","alias":"zipcode","type":"text","group":"core","object":"lead","is_fixed":"1","value":""},"country":{"id":"16","label":"Country","alias":"country","type":"country","group":"core","object":"lead","is_fixed":"1","value":""},"fax":{"id":"10","label":"Fax","alias":"fax","type":"tel","group":"core","object":"lead","is_fixed":"0","value":null},"preferred_locale":{"id":"17","label":"Preferred Locale","alias":"preferred_locale","type":"locale","group":"core","object":"lead","is_fixed":"1","value":null},"attribution_date":{"id":"18","label":"Attribution Date","alias":"attribution_date","type":"datetime","group":"core","object":"lead","is_fixed":"1","value":null},"attribution":{"id":"19","label":"Attribution","alias":"attribution","type":"number","group":"core","object":"lead","is_fixed":"1","value":null},"website":{"id":"20","label":"Website","alias":"website","type":"url","group":"core","object":"lead","is_fixed":"0","value":null},"boolean":{"id":"43","label":"Boolean","alias":"boolean","type":"boolean","group":"core","object":"lead","is_fixed":"0","value":null},"multiple_contact":{"id":"44","label":"Multiple Contact","alias":"multiple_contact","type":"multiselect","group":"core","object":"lead","is_fixed":"0","value":null}},"social":{"facebook":{"id":"21","label":"Facebook","alias":"facebook","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"foursquare":{"id":"22","label":"Foursquare","alias":"foursquare","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"instagram":{"id":"24","label":"Instagram","alias":"instagram","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"linkedin":{"id":"25","label":"LinkedIn","alias":"linkedin","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"skype":{"id":"26","label":"Skype","alias":"skype","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"twitter":{"id":"27","label":"Twitter","alias":"twitter","type":"text","group":"social","object":"lead","is_fixed":"0","value":null}},"personal":[],"professional":[]}},"channel":"email","old_status":"contactable","new_status":"manual","timestamp":"2017-12-01T00:05:18-06:00"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"contact":{"isPublished":true,"dateAdded":"2017-06-19T09:31:18+00:00","dateModified":"2017-06-19T09:33:58+00:00","createdBy":1,"createdByUser":"John Doe","modifiedBy":null,"modifiedByUser":" ","id":52,"points":5,"color":null,"fields":{"core":{"title":{"id":"1","label":"Title","alias":"title","type":"lookup","group":"core","object":"lead","is_fixed":"1","value":"Mr."},"firstname":{"id":"2","label":"First Name","alias":"firstname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"John"},"lastname":{"id":"3","label":"Last Name","alias":"lastname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Doe"},"company":{"id":"4","label":"Company","alias":"company","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Mautic"},"position":{"id":"5","label":"Position","alias":"position","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"email":{"id":"6","label":"Email","alias":"email","type":"email","group":"core","object":"lead","is_fixed":"1","value":"john@doe.name"},"mobile":{"id":"7","label":"Mobile","alias":"mobile","type":"tel","group":"core","object":"lead","is_fixed":"1","value":"333444555"},"phone":{"id":"8","label":"Phone","alias":"phone","type":"tel","group":"core","object":"lead","is_fixed":"1","value":null},"fax":{"id":"9","label":"Fax","alias":"fax","type":"tel","group":"core","object":"lead","is_fixed":"0","value":null},"address1":{"id":"10","label":"Address Line 1","alias":"address1","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"address2":{"id":"11","label":"Address Line 2","alias":"address2","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"city":{"id":"12","label":"City","alias":"city","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Prague"},"state":{"id":"13","label":"State","alias":"state","type":"region","group":"core","object":"lead","is_fixed":"1","value":null},"zipcode":{"id":"14","label":"Zip Code","alias":"zipcode","type":"text","group":"core","object":"lead","is_fixed":"1","value":"16000"},"country":{"id":"15","label":"Country","alias":"country","type":"country","group":"core","object":"lead","is_fixed":"1","value":"Czech Republic"},"preferred_locale":{"id":"16","label":"Preferred Locale","alias":"preferred_locale","type":"locale","group":"core","object":"lead","is_fixed":"1","value":"cs_CZ"},"attribution_date":{"id":"17","label":"Attribution Date","alias":"attribution_date","type":"datetime","group":"core","object":"lead","is_fixed":"1","value":"2017-06-14 11:30:00"},"attribution":{"id":"18","label":"Attribution","alias":"attribution","type":"number","group":"core","object":"lead","is_fixed":"1","value":"32"},"website":{"id":"19","label":"Website","alias":"website","type":"url","group":"core","object":"lead","is_fixed":"0","value":null},"multiselect":{"id":"42","label":"Multiselect","alias":"multiselect","type":"multiselect","group":"core","object":"lead","is_fixed":"0","value":"php|js"},"f_select":{"id":"43","label":"select","alias":"f_select","type":"select","group":"core","object":"lead","is_fixed":"0","value":null},"boolean":{"id":"44","label":"boolean","alias":"boolean","type":"boolean","group":"core","object":"lead","is_fixed":"0","value":null},"datetime":{"id":"45","label":"datetime","alias":"datetime","type":"datetime","group":"core","object":"lead","is_fixed":"0","value":null},"timezone1":{"id":"46","label":"timezone","alias":"timezone1","type":"timezone","group":"core","object":"lead","is_fixed":"0","value":"Europe\/Prague"}},"social":{"facebook":{"id":"20","label":"Facebook","alias":"facebook","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"foursquare":{"id":"21","label":"Foursquare","alias":"foursquare","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"instagram":{"id":"23","label":"Instagram","alias":"instagram","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"linkedin":{"id":"24","label":"LinkedIn","alias":"linkedin","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"skype":{"id":"25","label":"Skype","alias":"skype","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"twitter":{"id":"26","label":"Twitter","alias":"twitter","type":"text","group":"social","object":"lead","is_fixed":"0","value":null}},"personal":[],"professional":[]},"lastActive":"2017-06-19T09:33:58+00:00","owner":{"createdByUser":null,"modifiedByUser":null,"id":1,"username":"admin","firstName":"John","lastName":"Doe"},"ipAddresses":[{"ip":"127.0.0.1","id":1,"ipDetails":{"city":"","region":"","zipcode":"","country":"","latitude":"","longitude":"","isp":"","organization":"","timezone":"","extra":""}}],"tags":[[]],"utmtags":[],"stage":null,"dateIdentified":"2017-06-19T09:31:18+00:00","preferredProfileImage":"gravatar","doNotContact":[],"frequencyRules":[]},"timestamp":"2017-06-19T09:33:59+00:00"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"id":52,"contact":{"isPublished":true,"dateAdded":"2017-06-19T09:31:18+00:00","dateModified":"2017-06-19T09:33:58+00:00","createdBy":1,"createdByUser":"John Doe","modifiedBy":null,"modifiedByUser":" ","id":null,"points":5,"color":null,"fields":{"core":{"title":{"id":"1","label":"Title","alias":"title","type":"lookup","group":"core","object":"lead","is_fixed":"1","value":"Mr."},"firstname":{"id":"2","label":"First Name","alias":"firstname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"John"},"lastname":{"id":"3","label":"Last Name","alias":"lastname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Doe"},"company":{"id":"4","label":"Company","alias":"company","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Mautic"},"position":{"id":"5","label":"Position","alias":"position","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"email":{"id":"6","label":"Email","alias":"email","type":"email","group":"core","object":"lead","is_fixed":"1","value":"john@doe.name"},"mobile":{"id":"7","label":"Mobile","alias":"mobile","type":"tel","group":"core","object":"lead","is_fixed":"1","value":"333444555"},"phone":{"id":"8","label":"Phone","alias":"phone","type":"tel","group":"core","object":"lead","is_fixed":"1","value":null},"fax":{"id":"9","label":"Fax","alias":"fax","type":"tel","group":"core","object":"lead","is_fixed":"0","value":null},"address1":{"id":"10","label":"Address Line 1","alias":"address1","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"address2":{"id":"11","label":"Address Line 2","alias":"address2","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"city":{"id":"12","label":"City","alias":"city","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Prague"},"state":{"id":"13","label":"State","alias":"state","type":"region","group":"core","object":"lead","is_fixed":"1","value":null},"zipcode":{"id":"14","label":"Zip Code","alias":"zipcode","type":"text","group":"core","object":"lead","is_fixed":"1","value":"16000"},"country":{"id":"15","label":"Country","alias":"country","type":"country","group":"core","object":"lead","is_fixed":"1","value":"Czech Republic"},"preferred_locale":{"id":"16","label":"Preferred Locale","alias":"preferred_locale","type":"locale","group":"core","object":"lead","is_fixed":"1","value":"cs_CZ"},"attribution_date":{"id":"17","label":"Attribution Date","alias":"attribution_date","type":"datetime","group":"core","object":"lead","is_fixed":"1","value":"2017-06-14 11:30:00"},"attribution":{"id":"18","label":"Attribution","alias":"attribution","type":"number","group":"core","object":"lead","is_fixed":"1","value":"32"},"website":{"id":"19","label":"Website","alias":"website","type":"url","group":"core","object":"lead","is_fixed":"0","value":null},"multiselect":{"id":"42","label":"Multiselect","alias":"multiselect","type":"multiselect","group":"core","object":"lead","is_fixed":"0","value":"php|js"},"f_select":{"id":"43","label":"select","alias":"f_select","type":"select","group":"core","object":"lead","is_fixed":"0","value":null},"boolean":{"id":"44","label":"boolean","alias":"boolean","type":"boolean","group":"core","object":"lead","is_fixed":"0","value":null},"datetime":{"id":"45","label":"datetime","alias":"datetime","type":"datetime","group":"core","object":"lead","is_fixed":"0","value":null},"timezone1":{"id":"46","label":"timezone","alias":"timezone1","type":"timezone","group":"core","object":"lead","is_fixed":"0","value":"Europe\/Prague"}},"social":{"facebook":{"id":"20","label":"Facebook","alias":"facebook","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"foursquare":{"id":"21","label":"Foursquare","alias":"foursquare","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"instagram":{"id":"23","label":"Instagram","alias":"instagram","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"linkedin":{"id":"24","label":"LinkedIn","alias":"linkedin","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"skype":{"id":"25","label":"Skype","alias":"skype","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"twitter":{"id":"26","label":"Twitter","alias":"twitter","type":"text","group":"social","object":"lead","is_fixed":"0","value":null}},"personal":[],"professional":[]},"lastActive":"2017-06-19T09:33:58+00:00","owner":{"createdByUser":null,"modifiedByUser":null,"id":1,"username":"admin","firstName":"John","lastName":"Doe"},"ipAddresses":[{"ip":"127.0.0.1","id":1,"ipDetails":{"city":"","region":"","zipcode":"","country":"","latitude":"","longitude":"","isp":"","organization":"","timezone":"","extra":""}}],"tags":[],"utmtags":[],"stage":null,"dateIdentified":"2017-06-19T09:31:18+00:00","preferredProfileImage":"gravatar","doNotContact":[],"frequencyRules":[]},"timestamp":"2017-06-19T09:34:55+00:00"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"contact":{"isPublished":true,"dateAdded":"2017-06-19T09:31:18+00:00","dateModified":null,"createdBy":1,"createdByUser":"John Doe","modifiedBy":null,"modifiedByUser":null,"id":52,"points":0,"color":null,"fields":{"core":{"timezone1":{"id":46,"group":"core","label":"timezone","alias":"timezone1","type":"timezone","value":"Europe\/Prague"},"datetime":{"id":45,"group":"core","label":"datetime","alias":"datetime","type":"datetime","value":""},"boolean":{"id":44,"group":"core","label":"boolean","alias":"boolean","type":"boolean","value":null},"f_select":{"id":43,"group":"core","label":"select","alias":"f_select","type":"select","value":null},"multiselect":{"id":42,"group":"core","label":"Multiselect","alias":"multiselect","type":"multiselect","value":"php|js"},"title":{"id":1,"group":"core","label":"Title","alias":"title","type":"lookup","value":"Mr."},"firstname":{"id":2,"group":"core","label":"First Name","alias":"firstname","type":"text","value":"John"},"lastname":{"id":3,"group":"core","label":"Last Name","alias":"lastname","type":"text","value":"Doe"},"company":{"id":4,"group":"core","label":"Company","alias":"company","type":"text","value":null},"position":{"id":5,"group":"core","label":"Position","alias":"position","type":"text","value":null},"email":{"id":6,"group":"core","label":"Email","alias":"email","type":"email","value":"john@doe.name"},"mobile":{"id":7,"group":"core","label":"Mobile","alias":"mobile","type":"tel","value":null},"phone":{"id":8,"group":"core","label":"Phone","alias":"phone","type":"tel","value":null},"fax":{"id":9,"group":"core","label":"Fax","alias":"fax","type":"tel","value":null},"address1":{"id":10,"group":"core","label":"Address Line 1","alias":"address1","type":"text","value":null},"address2":{"id":11,"group":"core","label":"Address Line 2","alias":"address2","type":"text","value":null},"city":{"id":12,"group":"core","label":"City","alias":"city","type":"text","value":"Prague"},"state":{"id":13,"group":"core","label":"State","alias":"state","type":"region","value":null},"zipcode":{"id":14,"group":"core","label":"Zip Code","alias":"zipcode","type":"text","value":"16000"},"country":{"id":15,"group":"core","label":"Country","alias":"country","type":"country","value":"Czech Republic"},"preferred_locale":{"id":16,"group":"core","label":"Preferred Locale","alias":"preferred_locale","type":"locale","value":"cs_CZ"},"attribution_date":{"id":17,"group":"core","label":"Attribution Date","alias":"attribution_date","type":"datetime","value":"2017-06-14 11:30:00"},"attribution":{"id":18,"group":"core","label":"Attribution","alias":"attribution","type":"number","value":32},"website":{"id":19,"group":"core","label":"Website","alias":"website","type":"url","value":null}},"social":{"facebook":{"id":20,"group":"social","label":"Facebook","alias":"facebook","type":"text","value":null},"foursquare":{"id":21,"group":"social","label":"Foursquare","alias":"foursquare","type":"text","value":null},"instagram":{"id":23,"group":"social","label":"Instagram","alias":"instagram","type":"text","value":null},"linkedin":{"id":24,"group":"social","label":"LinkedIn","alias":"linkedin","type":"text","value":null},"skype":{"id":25,"group":"social","label":"Skype","alias":"skype","type":"text","value":null},"twitter":{"id":26,"group":"social","label":"Twitter","alias":"twitter","type":"text","value":null}},"personal":[],"professional":[]},"lastActive":null,"owner":{"createdByUser":null,"modifiedByUser":null,"id":1,"username":"admin","firstName":"John","lastName":"Doe"},"ipAddresses":[],"tags":[[]],"utmtags":null,"stage":null,"dateIdentified":"2017-06-19T09:31:18+00:00","preferredProfileImage":"gravatar","doNotContact":[],"frequencyRules":[]},"timestamp":"2017-06-19T09:31:18+00:00"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"contact":{"isPublished":true,"dateAdded":"2017-06-19T09:31:18+00:00","dateModified":"2017-06-19T09:32:24+00:00","createdBy":1,"createdByUser":"John Doe","modifiedBy":1,"modifiedByUser":"John Doe","id":52,"points":0,"color":null,"fields":{"core":{"title":{"id":"1","label":"Title","alias":"title","type":"lookup","group":"core","object":"lead","is_fixed":"1","value":"Mr."},"firstname":{"id":"2","label":"First Name","alias":"firstname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"John"},"lastname":{"id":"3","label":"Last Name","alias":"lastname","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Doe"},"company":{"id":"4","label":"Company","alias":"company","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Mautic"},"position":{"id":"5","label":"Position","alias":"position","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"email":{"id":"6","label":"Email","alias":"email","type":"email","group":"core","object":"lead","is_fixed":"1","value":"john@doe.name"},"mobile":{"id":"7","label":"Mobile","alias":"mobile","type":"tel","group":"core","object":"lead","is_fixed":"1","value":"333444555"},"phone":{"id":"8","label":"Phone","alias":"phone","type":"tel","group":"core","object":"lead","is_fixed":"1","value":null},"fax":{"id":"9","label":"Fax","alias":"fax","type":"tel","group":"core","object":"lead","is_fixed":"0","value":null},"address1":{"id":"10","label":"Address Line 1","alias":"address1","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"address2":{"id":"11","label":"Address Line 2","alias":"address2","type":"text","group":"core","object":"lead","is_fixed":"1","value":null},"city":{"id":"12","label":"City","alias":"city","type":"text","group":"core","object":"lead","is_fixed":"1","value":"Prague"},"state":{"id":"13","label":"State","alias":"state","type":"region","group":"core","object":"lead","is_fixed":"1","value":null},"zipcode":{"id":"14","label":"Zip Code","alias":"zipcode","type":"text","group":"core","object":"lead","is_fixed":"1","value":"16000"},"country":{"id":"15","label":"Country","alias":"country","type":"country","group":"core","object":"lead","is_fixed":"1","value":"Czech Republic"},"preferred_locale":{"id":"16","label":"Preferred Locale","alias":"preferred_locale","type":"locale","group":"core","object":"lead","is_fixed":"1","value":"cs_CZ"},"attribution_date":{"id":"17","label":"Attribution Date","alias":"attribution_date","type":"datetime","group":"core","object":"lead","is_fixed":"1","value":"2017-06-14 11:30:00"},"attribution":{"id":"18","label":"Attribution","alias":"attribution","type":"number","group":"core","object":"lead","is_fixed":"1","value":32},"website":{"id":"19","label":"Website","alias":"website","type":"url","group":"core","object":"lead","is_fixed":"0","value":null},"multiselect":{"id":"42","label":"Multiselect","alias":"multiselect","type":"multiselect","group":"core","object":"lead","is_fixed":"0","value":"php|js"},"f_select":{"id":"43","label":"select","alias":"f_select","type":"select","group":"core","object":"lead","is_fixed":"0","value":null},"boolean":{"id":"44","label":"boolean","alias":"boolean","type":"boolean","group":"core","object":"lead","is_fixed":"0","value":null},"datetime":{"id":"45","label":"datetime","alias":"datetime","type":"datetime","group":"core","object":"lead","is_fixed":"0","value":""},"timezone1":{"id":"46","label":"timezone","alias":"timezone1","type":"timezone","group":"core","object":"lead","is_fixed":"0","value":"Europe\/Prague"}},"social":{"facebook":{"id":"20","label":"Facebook","alias":"facebook","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"foursquare":{"id":"21","label":"Foursquare","alias":"foursquare","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"instagram":{"id":"23","label":"Instagram","alias":"instagram","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"linkedin":{"id":"24","label":"LinkedIn","alias":"linkedin","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"skype":{"id":"25","label":"Skype","alias":"skype","type":"text","group":"social","object":"lead","is_fixed":"0","value":null},"twitter":{"id":"26","label":"Twitter","alias":"twitter","type":"text","group":"social","object":"lead","is_fixed":"0","value":null}},"personal":[],"professional":[]},"lastActive":null,"owner":{"createdByUser":null,"modifiedByUser":null,"id":1,"username":"admin","firstName":"John","lastName":"Doe"},"ipAddresses":[],"tags":[[]],"utmtags":[],"stage":null,"dateIdentified":"2017-06-19T09:31:18+00:00","preferredProfileImage":"gravatar","doNotContact":[],"frequencyRules":[]},"timestamp":"2017-06-19T09:32:24+00:00"}]
|
||||
@@ -0,0 +1,2 @@
|
||||
points,title,firstname,lastname,company,position,email,phone,mobile,address1,address2,city,state,zipcode,timezone,country,fax,preferred_locale,attribution_date,attribution,website,facebook,foursquare,instagram,linkedin,skype,twitter,stage
|
||||
2520,,Dell,Joost,ABC Company,Marketing Lead,djoost5j@example.com,,57912345678,ABC Street,ABC District,ABC City,Sao Paulo,79270-000,America/Sao_Paulo,Brazil,,pt_BR,,,http://example.com,,,,,,,
|
||||
|
@@ -0,0 +1,389 @@
|
||||
/* LeadBundle */
|
||||
|
||||
.col-leadfield-id, .col-leadlist-id, .col-lead-id {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.col-leadfield-statusicons, .col-leadlist-leadcount {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.col-leadfield-orderhandle {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.col-leadfield-type {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
.leadfield-list i {
|
||||
color: #7D5B71;
|
||||
}
|
||||
|
||||
.segment-filter {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.segment-filter .filter-buttons {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: -1px;
|
||||
min-width: 100px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.leadlist-filter a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.leadlist-filter:hover {
|
||||
background-color: #ecf0f1;
|
||||
}
|
||||
|
||||
.leadlist-filter {
|
||||
height: 40px;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leadlist-filter-name {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.available-filters .rounded-corners {
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.selected-filters .rounded-corners {
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.selected-filters .in-group {
|
||||
margin-left:20px;
|
||||
}
|
||||
|
||||
.selected-filters .panel {
|
||||
margin-bottom:0;
|
||||
}
|
||||
|
||||
.selected-filters .panel.in-group {
|
||||
margin-top:0;
|
||||
border-top:0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.col-leadpoints-date {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.col-leadpoints-delta {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.panel-foursquare .panel-heading {
|
||||
background: #0072b1;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.panel-facebook .panel-heading {
|
||||
background: #3b5998;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.panel-linkedin .panel-heding {
|
||||
background: #007bb6;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.panel-twitter .panel-heading {
|
||||
background: #00aced;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.panel-instagram .panel-heading {
|
||||
background: #517fa4;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
ul.tag-cloud li {
|
||||
list-style-type: none;
|
||||
float:left;
|
||||
margin: 0 10px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
#social-container div.tab-pane {
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.card.highlight {
|
||||
background-color: #faf2cc;
|
||||
border: 3px solid #4e5d9d !important;
|
||||
}
|
||||
|
||||
.lead-avatar-panel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lead-avatar-panel .avatar-collapser a.arrow {
|
||||
position: absolute;
|
||||
top:0;
|
||||
right: 0px;
|
||||
margin: 0 3px 0 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.lead-avatar-panel .img-responsive {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.lead-merge-options {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contact-cards .panel-body {
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.contact-cards .img {
|
||||
max-height: 115px;
|
||||
}
|
||||
|
||||
#lead_merge_lead_to_merge_chosen .chosen-search {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.history-search{
|
||||
overflow: visible!important;
|
||||
min-height: 95px;
|
||||
}
|
||||
|
||||
.timeline-icon, .table-expand {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.timeline-featured td, .timeline-featured th {
|
||||
background: rgba(78, 94, 158, 0.1 );
|
||||
}
|
||||
|
||||
#contact-timeline td {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-all;
|
||||
-ms-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
#contact-timeline .dl-horizontal dt {
|
||||
width: auto;
|
||||
text-align: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#contact-timeline .dl-horizontal dd {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
#contact-timeline tr.timeline-details img {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.frequency{
|
||||
width: 65px!important;
|
||||
}
|
||||
|
||||
.contact{
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
.frequency-table, .frequency-table td{
|
||||
border-bottom: none!important;
|
||||
border-top: none!important;
|
||||
}
|
||||
|
||||
.frequency-values {
|
||||
padding-left: 10px;
|
||||
padding-top: 27px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.frequency-select {
|
||||
width:125px !important;
|
||||
float: right;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.frequency-label {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.frequency-date {
|
||||
width: 100px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.properties-form .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.properties-form:has(.alert) .row:nth-child(2) {
|
||||
position: absolute;
|
||||
left: 82px;
|
||||
margin-top: 19px;
|
||||
}
|
||||
|
||||
@media (min-width: 615px) and (max-width: 770px) {
|
||||
.properties-form:has(.alert) .row:nth-child(2) {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 820px) {
|
||||
.properties-form:has(.alert) .row:nth-child(2) {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter--row { margin-bottom: 15px; }
|
||||
|
||||
.filter--row:has(.alert) { margin-bottom: 125px; }
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.filter--row:has(.alert) { margin-bottom: 85px; }
|
||||
}
|
||||
|
||||
@media (min-width: 1250px) {
|
||||
.filter--row:has(.alert) { margin-bottom: 65px; }
|
||||
}
|
||||
|
||||
.filter--row select.form-control,
|
||||
.filter--row select.glue-select {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 15.6315L20.9679 10.8838L20.0321 9.11619L12 13.3685L3.9679 9.11619L3.03212 10.8838L12 15.6315Z'%3E%3C/path%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 10px;
|
||||
background-position-y: center;
|
||||
|
||||
padding-right: 24px;
|
||||
background-position-x: 84%;
|
||||
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.filter--panel {
|
||||
border-color: var(--border-subtle);
|
||||
}
|
||||
.filter--panel select:focus {
|
||||
outline: 2px solid var(--focus);
|
||||
}
|
||||
|
||||
.filter--panel select,
|
||||
.filter--panel input {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: var(--spacing-01) var(--spacing-06);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filter--panel select.form-control,
|
||||
.filter--panel input,
|
||||
.filter--panel .chosen-container-multi .chosen-choices {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.filter--panel select.form-control,
|
||||
.filter--panel input {
|
||||
padding: var(--spacing-01) var(--spacing-06);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filter--field-operator {
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
border-right: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.filter--field-name {
|
||||
min-height: 40px;
|
||||
background-color: var(--layer);
|
||||
}
|
||||
|
||||
.filter--panel .help-block {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: 18px;
|
||||
}
|
||||
|
||||
.filter--panel select.form-control {
|
||||
padding-right: 40px;
|
||||
background-position-x: calc(100% - 20px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-companies .primary {
|
||||
color: #fdb933;
|
||||
}
|
||||
|
||||
.building::before {
|
||||
content: "\eb08";
|
||||
font-family: "RemixIcon";
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.user::before {
|
||||
content: "\f25b";
|
||||
font-family: "RemixIcon";
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
#leads-container .spinner {
|
||||
text-align: center;
|
||||
line-height: 250px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.segment-node {
|
||||
border: grey solid 1px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
flex: 1 200px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 200px;
|
||||
flex-flow: column wrap;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.segment-node.has-message {
|
||||
border: red solid 4px;
|
||||
}
|
||||
|
||||
.segment-level {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lead-stats-title {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\ExitCode;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class CleanupExportedFilesCommand extends Command
|
||||
{
|
||||
public const COMMAND_NAME = 'mautic:contacts:cleanup_exported_files';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const CLEANUP_DAYS = 'cleanupAfterDays';
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::COMMAND_NAME)
|
||||
->setDescription('Remove contact export cache files from `contacts_export` directory if file is older than the week/7 days')
|
||||
->addArgument(self::CLEANUP_DAYS, InputArgument::OPTIONAL, 'Remove exported files after days');
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$days = $input->getArgument(self::CLEANUP_DAYS);
|
||||
if (!$days) {
|
||||
$days = $this->coreParametersHelper->get('clear_export_files_after_days');
|
||||
}
|
||||
|
||||
$dateHelper = new DateTimeHelper();
|
||||
$date = $dateHelper->getUtcDateTime()->modify('-'.(int) $days.' days');
|
||||
$cleanUpTimestamp = $date->getTimestamp();
|
||||
|
||||
$downloadFolder = $this->coreParametersHelper->get('contact_export_dir');
|
||||
$contactExportedAllFiles = glob($downloadFolder.'/contacts_export_*');
|
||||
|
||||
foreach ($contactExportedAllFiles as $file) {
|
||||
if (filectime($file) <= $cleanUpTimestamp) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ExitCode;
|
||||
use Mautic\CoreBundle\ProcessSignal\Exception\SignalCaughtException;
|
||||
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
|
||||
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
|
||||
use Mautic\LeadBundle\Event\ContactExportSchedulerEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Model\ContactExportSchedulerModel;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: ContactScheduledExportCommand::COMMAND_NAME,
|
||||
description: 'Export contacts which are scheduled in `contact_export_scheduler` table.'
|
||||
)]
|
||||
class ContactScheduledExportCommand extends Command
|
||||
{
|
||||
private const PICK_SCHEDULED_EXPORTS_LIMIT = 10;
|
||||
|
||||
public const COMMAND_NAME = 'mautic:contacts:scheduled_export';
|
||||
|
||||
public function __construct(
|
||||
private ContactExportSchedulerModel $contactExportSchedulerModel,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private FormatterHelper $formatterHelper,
|
||||
private ProcessSignalService $processSignalService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'--ids',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Comma separated contact_export_scheduler ids.'
|
||||
);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->processSignalService->registerSignalHandler(
|
||||
fn (int $signal) => $output->writeln(sprintf('Signal %d caught.', $signal))
|
||||
);
|
||||
|
||||
$ids = $this->formatterHelper->simpleCsvToArray($input->getOption('ids'), 'int');
|
||||
|
||||
if ($ids) {
|
||||
$contactExportSchedulers = $this->contactExportSchedulerModel->getRepository()->findBy(['id' => $ids]);
|
||||
} else {
|
||||
$contactExportSchedulers = $this->contactExportSchedulerModel->getRepository()
|
||||
->findBy([], [], self::PICK_SCHEDULED_EXPORTS_LIMIT);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
try {
|
||||
foreach ($contactExportSchedulers as $contactExportScheduler) {
|
||||
$contactExportSchedulerEvent = new ContactExportSchedulerEvent($contactExportScheduler);
|
||||
$this->eventDispatcher->dispatch($contactExportSchedulerEvent, LeadEvents::CONTACT_EXPORT_PREPARE_FILE);
|
||||
$this->eventDispatcher->dispatch($contactExportSchedulerEvent, LeadEvents::CONTACT_EXPORT_SEND_EMAIL);
|
||||
$this->eventDispatcher->dispatch($contactExportSchedulerEvent, LeadEvents::POST_CONTACT_EXPORT_SEND_EMAIL);
|
||||
++$count;
|
||||
}
|
||||
|
||||
$output->writeln('Contact export email(s) sent: '.$count);
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
} catch (SignalCaughtException) {
|
||||
return ExitCode::TERMINATED;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Service\ProcessQueue;
|
||||
use Mautic\LeadBundle\Deduplicate\ContactDeduper;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
#[AsCommand(
|
||||
name: DeduplicateCommand::NAME,
|
||||
description: 'Merge contacts based on same unique identifiers'
|
||||
)]
|
||||
class DeduplicateCommand extends Command
|
||||
{
|
||||
public const NAME = 'mautic:contacts:deduplicate';
|
||||
|
||||
public function __construct(
|
||||
private ContactDeduper $contactDeduper,
|
||||
private ParameterBagInterface $params,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->addOption(
|
||||
'--newer-into-older',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'By default, this command will merge older contacts and activity into the newer. Use this flag to reverse that behavior.'
|
||||
)
|
||||
->addOption(
|
||||
'--batch',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'How many contact duplicates to process at once. Defaults to 100.',
|
||||
100
|
||||
)
|
||||
->addOption(
|
||||
'--processes',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The commands can run in multiple PHP processes. This option defines how many processes to run. Defaults to 1.',
|
||||
1
|
||||
)
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command will dedpulicate contacts based on unique identifier values.
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$newerIntoOlder = (bool) $input->getOption('newer-into-older');
|
||||
$batch = (int) $input->getOption('batch');
|
||||
$processes = (int) $input->getOption('processes');
|
||||
$uniqueFields = $this->contactDeduper->getUniqueFields('lead');
|
||||
$duplicateCount = $this->contactDeduper->countDuplicatedContacts(array_keys($uniqueFields));
|
||||
$stopwatch = new Stopwatch();
|
||||
|
||||
if (!$duplicateCount) {
|
||||
$output->writeln('<error>No contacts to deduplicate.</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$stopwatch->start('deduplicate');
|
||||
|
||||
$output->writeln('Deduplicating contacts based on unique identifiers: '.implode(', ', $uniqueFields));
|
||||
$output->writeln("{$duplicateCount} contacts found to deduplicate");
|
||||
|
||||
$processQueue = new ProcessQueue($processes);
|
||||
$processCount = (int) ceil($duplicateCount / $batch);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln("Finding duplicates and creating processes for deduplication. {$processCount} processes will be queued.");
|
||||
|
||||
$contactIds = $this->contactDeduper->getDuplicateContactIds(array_keys($uniqueFields));
|
||||
$contactIdChunks = array_chunk($contactIds, $batch);
|
||||
foreach ($contactIdChunks as $contactIdBatch) {
|
||||
$command = [
|
||||
$this->params->get('kernel.project_dir').'/bin/console',
|
||||
DeduplicateIdsCommand::NAME,
|
||||
'--contact-ids',
|
||||
implode(',', $contactIdBatch),
|
||||
'-e',
|
||||
MAUTIC_ENV,
|
||||
];
|
||||
|
||||
if ($newerIntoOlder) {
|
||||
$command[] = '--newer-into-older';
|
||||
}
|
||||
|
||||
$envParams = [
|
||||
'db_table_prefix' => MAUTIC_TABLE_PREFIX,
|
||||
'contact_unique_identifiers_operator' => $this->params->get('mautic.contact_unique_identifiers_operator'),
|
||||
];
|
||||
|
||||
$processQueue->enqueue(new Process($command, null, ['MAUTIC_CONFIG_PARAMETERS' => json_encode($envParams)]));
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln("Starting to execute the {$processCount} processes for deduplication. {$processes} processes will be executed in parallel.");
|
||||
|
||||
$progressBar = new ProgressBar($output, $processCount);
|
||||
$progressBar->setFormat('debug');
|
||||
$progressBar->start();
|
||||
|
||||
$processQueue->refresh();
|
||||
|
||||
while ($processQueue->isProcessing()) {
|
||||
usleep(100);
|
||||
$processQueue->refresh();
|
||||
$progressBar->setProgress($processQueue->getProcessedCount());
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('');
|
||||
$output->writeln('All processes have finished. The output of each process is below.');
|
||||
|
||||
foreach ($processQueue->getProcessed() as $process) {
|
||||
$output->writeln("<comment>{$process->getCommandLine()}</comment>");
|
||||
if (0 === $process->getExitCode()) {
|
||||
$output->writeln("<info>{$process->getOutput()}</info>");
|
||||
} else {
|
||||
$output->writeln("<error>{$process->getErrorOutput()}</error>");
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
$event = $stopwatch->stop('deduplicate');
|
||||
$output->writeln('');
|
||||
$output->writeln("Duration: {$event->getDuration()} ms, Memory: {$event->getMemory()} bytes");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\LeadBundle\Deduplicate\ContactDeduper;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
#[AsCommand(
|
||||
name: DeduplicateIdsCommand::NAME,
|
||||
description: 'Merge contacts based on same unique identifiers'
|
||||
)]
|
||||
class DeduplicateIdsCommand extends Command
|
||||
{
|
||||
public const NAME = 'mautic:contacts:deduplicate:ids';
|
||||
|
||||
public function __construct(
|
||||
private ContactDeduper $contactDeduper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->addOption(
|
||||
'--newer-into-older',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'By default, this command will merge older contacts and activity into the newer. Use this flag to reverse that behavior.'
|
||||
)
|
||||
->addOption(
|
||||
'--contact-ids',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Comma separated list of contact IDs to deduplicate. If not provided, all contacts will be deduplicated. Example: --contact-ids=23,3,11'
|
||||
)
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command will dedpulicate contacts based on unique identifier values.
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$newerIntoOlder = (bool) $input->getOption('newer-into-older');
|
||||
$contactIds = array_filter(explode(',', $input->getOption('contact-ids')));
|
||||
$duplicateCount = count($contactIds);
|
||||
$progressBar = new ProgressBar($output, $duplicateCount);
|
||||
$stopwatch = new Stopwatch();
|
||||
|
||||
if (!$contactIds) {
|
||||
$output->writeln('<error>No contacts to deduplicate.</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$output->writeln("{$duplicateCount} contacts passed to deduplicate");
|
||||
|
||||
$progressBar->setFormat('debug');
|
||||
$progressBar->start();
|
||||
$stopwatch->start('deduplicate');
|
||||
|
||||
$contacts = $this->contactDeduper->getContactsByIds($contactIds);
|
||||
$this->contactDeduper->deduplicateContactBatch($contacts, $newerIntoOlder, fn () => $progressBar->advance());
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
$event = $stopwatch->stop('deduplicate');
|
||||
$output->writeln("Duration: {$event->getDuration()} ms, Memory: {$event->getMemory()} bytes");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: DeleteContactSecondaryCompaniesCommand::NAME,
|
||||
description: "Deletes all contact\'s secondary companies."
|
||||
)]
|
||||
class DeleteContactSecondaryCompaniesCommand extends Command
|
||||
{
|
||||
public const NAME = 'mautic:contact:delete:secondary-companies';
|
||||
|
||||
public function __construct(private LoggerInterface $logger, private TranslatorInterface $translator, private CoreParametersHelper $coreParametersHelper, private CompanyLeadRepository $companyLeadsRepository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command deletes non-primary companies of every contact.
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$allowMultiple = $this->coreParametersHelper->get('contact_allow_multiple_companies');
|
||||
|
||||
// We process only if the config is set to false
|
||||
if ($allowMultiple) {
|
||||
$output->writeln($this->translator->trans('mautic.lead.command.delete_contact_secondary_company.allow_multiple_enabled'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->companyLeadsRepository->removeAllSecondaryCompanies();
|
||||
} catch (ORMException $e) {
|
||||
$errorMessage = $this->translator->trans('mautic.lead.command.error', ['%name%' => self::NAME, '%error%' => $e->getMessage()]);
|
||||
$output->writeln($errorMessage);
|
||||
$this->logger->error($errorMessage);
|
||||
}
|
||||
|
||||
$output->writeln($this->translator->trans('mautic.lead.command.delete_contact_secondary_company.success'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Model\NotificationModel;
|
||||
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Exception\ImportDelayedException;
|
||||
use Mautic\LeadBundle\Exception\ImportFailedException;
|
||||
use Mautic\LeadBundle\Helper\Progress;
|
||||
use Mautic\LeadBundle\Model\ImportModel;
|
||||
use Mautic\UserBundle\Security\UserTokenSetter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* CLI Command to import data.
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: ImportCommand::COMMAND_NAME,
|
||||
description: 'Imports data to Mautic'
|
||||
)]
|
||||
class ImportCommand extends Command
|
||||
{
|
||||
public const COMMAND_NAME = 'mautic:import';
|
||||
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private ImportModel $importModel,
|
||||
private ProcessSignalService $processSignalService,
|
||||
private UserTokenSetter $userTokenSetter,
|
||||
private LoggerInterface $logger,
|
||||
private NotificationModel $notificationModel,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('--id', '-i', InputOption::VALUE_OPTIONAL, 'Specific ID to import. Defaults to next in the queue.', false)
|
||||
->addOption('--limit', '-l', InputOption::VALUE_OPTIONAL, 'Maximum number of records to import for this script execution.', 0)
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command starts to import CSV files when some are created.
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$start = microtime(true);
|
||||
$progress = new Progress($output);
|
||||
$id = (int) $input->getOption('id');
|
||||
$limit = (int) $input->getOption('limit');
|
||||
|
||||
$this->processSignalService->registerSignalHandler(fn (int $signal) => $output->writeln(sprintf('Signal %d caught.', $signal)));
|
||||
|
||||
if ($id) {
|
||||
$import = $this->importModel->getEntity($id);
|
||||
|
||||
// This specific import was not found
|
||||
if (!$import) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.core.error.notfound', [], 'flashes').'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$import = $this->importModel->getImportToProcess();
|
||||
|
||||
// No import waiting in the queue. Finish silently.
|
||||
if (null === $import) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$user = $import->getModifiedBy();
|
||||
|
||||
if (!$user) {
|
||||
throw new \RuntimeException('Import does not have "modifiedBy" property set.');
|
||||
}
|
||||
|
||||
$this->userTokenSetter->setUser($user);
|
||||
|
||||
$output->writeln('<info>'.$this->translator->trans(
|
||||
'mautic.lead.import.is.starting',
|
||||
[
|
||||
'%id%' => $import->getId(),
|
||||
'%lines%' => $import->getLineCount(),
|
||||
]
|
||||
).'</info>');
|
||||
|
||||
try {
|
||||
$this->importModel->beginImport($import, $progress, $limit, $start);
|
||||
} catch (ImportFailedException $e) {
|
||||
$output->writeln('<error>'.$this->translator->trans(
|
||||
'mautic.lead.import.failed',
|
||||
[
|
||||
'%reason%' => $import->getStatusInfo(),
|
||||
]
|
||||
).'</error>');
|
||||
|
||||
$this->logError($import, $e);
|
||||
|
||||
$this->notify(
|
||||
$import,
|
||||
$start,
|
||||
$this->translator->trans('mautic.lead.import.failed', ['%reason%' => $import->getStatusInfo()]),
|
||||
'error'
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
} catch (ImportDelayedException $e) {
|
||||
$output->writeln('<info>'.$this->translator->trans(
|
||||
'mautic.lead.import.delayed',
|
||||
[
|
||||
'%reason%' => $import->getStatusInfo(),
|
||||
]
|
||||
).'</info>');
|
||||
|
||||
$this->logError($import, $e);
|
||||
|
||||
$this->notify(
|
||||
$import,
|
||||
$start,
|
||||
$this->translator->trans('mautic.lead.import.delayed', ['%reason%' => $import->getStatusInfo()]),
|
||||
'warning'
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Success
|
||||
$output->writeln('<info>'.$this->translator->trans(
|
||||
'mautic.lead.import.result',
|
||||
[
|
||||
'%lines%' => $import->getProcessedRows(),
|
||||
'%created%' => $import->getInsertedCount(),
|
||||
'%updated%' => $import->getUpdatedCount(),
|
||||
'%ignored%' => $import->getIgnoredCount(),
|
||||
'%time%' => round(microtime(true) - $start, 2),
|
||||
]
|
||||
).'</info>');
|
||||
|
||||
// Notification is now handled in ImportModel::beginImport to avoid duplicates
|
||||
// and to include the link to the imported file
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function logError(Import $import, \Exception $exception): void
|
||||
{
|
||||
$message = ' Import id: '.$import->getId();
|
||||
$message .= ' Import Status: '.$import->getStatus();
|
||||
$message .= ' Reason: '.$import->getStatusInfo();
|
||||
$message .= ' Exception: '.$exception;
|
||||
|
||||
$this->logger->warning($message);
|
||||
}
|
||||
|
||||
private function notify(Import $import, float $start, string $header, string $type = 'info'): void
|
||||
{
|
||||
$this->notificationModel->addNotification(
|
||||
$this->translator->trans(
|
||||
'mautic.lead.import.result',
|
||||
[
|
||||
'%lines%' => $import->getProcessedRows(),
|
||||
'%created%' => $import->getInsertedCount(),
|
||||
'%updated%' => $import->getUpdatedCount(),
|
||||
'%ignored%' => $import->getIgnoredCount(),
|
||||
'%time%' => round(microtime(true) - $start, 2),
|
||||
]
|
||||
),
|
||||
$type,
|
||||
false,
|
||||
$header,
|
||||
'ri-download-line'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ExitCode;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class SegmentCountCacheCommand extends Command
|
||||
{
|
||||
public const COMMAND_NAME = 'lead:list:count-cache-update';
|
||||
|
||||
public function __construct(
|
||||
private LeadListRepository $leadListRepository,
|
||||
private SegmentCountCacheHelper $segmentCountCacheHelper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::COMMAND_NAME)
|
||||
->setDescription('Update segment count cache for changed segments.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$segmentsForRecount = $this->getAllSegmentsForRecount();
|
||||
if (count($segmentsForRecount) > 0) {
|
||||
$totalLeadCount = $this->leadListRepository->getLeadCount($segmentsForRecount);
|
||||
if (!is_array($totalLeadCount)) {
|
||||
$totalLeadCount = [$segmentsForRecount[0] => $totalLeadCount];
|
||||
}
|
||||
foreach ($totalLeadCount as $segmentId => $leadCount) {
|
||||
$this->segmentCountCacheHelper->setSegmentContactCount((int) $segmentId, (int) $leadCount);
|
||||
}
|
||||
}
|
||||
$output->writeln(sprintf('<info>%s segment\'s contact count have been updated.</info>', count($segmentsForRecount)));
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private function getAllSegmentsForRecount(): array
|
||||
{
|
||||
$segmentsForRecount = [];
|
||||
$segmentIds = $this->leadListRepository->getLists();
|
||||
foreach ($segmentIds as $segment) {
|
||||
$segmentId = $segment['id'];
|
||||
if ($this->segmentCountCacheHelper->hasSegmentIdForReCount($segmentId)) {
|
||||
$segmentsForRecount[] = $segmentId;
|
||||
}
|
||||
}
|
||||
|
||||
return $segmentsForRecount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Command\ModeratedCommand;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\LeadBundle\Event\GetStatDataEvent;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class SegmentStatCommand extends ModeratedCommand
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
PathsHelper $pathsHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($pathsHelper, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('mautic:segments:stat')
|
||||
->setDescription('Gather Segment Statistics');
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$event = new GetStatDataEvent();
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
if (empty($event->getResults())) {
|
||||
$io->write('There is no segment to show!!');
|
||||
} else {
|
||||
$io->table([
|
||||
'Title',
|
||||
'Id',
|
||||
'IsPublished',
|
||||
'IsUsed',
|
||||
],
|
||||
$event->getResults()
|
||||
);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Command\ModeratedCommand;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: UpdateLeadListsCommand::NAME,
|
||||
description: 'Update contacts in smart segments based on new contact data.',
|
||||
aliases: ['mautic:segments:rebuild']
|
||||
)]
|
||||
class UpdateLeadListsCommand extends ModeratedCommand
|
||||
{
|
||||
public const NAME = 'mautic:segments:update';
|
||||
|
||||
public function __construct(
|
||||
private ListModel $listModel,
|
||||
private TranslatorInterface $translator,
|
||||
PathsHelper $pathsHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($pathsHelper, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'--batch-limit',
|
||||
'-b',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Set batch size of contacts to process per round. Defaults to 300.',
|
||||
300
|
||||
)
|
||||
->addOption(
|
||||
'--max-contacts',
|
||||
'-m',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Set max number of contacts to process per segment for this script execution. Defaults to all.',
|
||||
false
|
||||
)
|
||||
->addOption(
|
||||
'--list-id',
|
||||
'-i',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Specific ID to rebuild. Defaults to all.',
|
||||
false
|
||||
)
|
||||
->addOption(
|
||||
'--timing',
|
||||
'-tm',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Measure timing of build with output to CLI .',
|
||||
false
|
||||
)
|
||||
->addOption(
|
||||
'exclude',
|
||||
'd',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
|
||||
'Exclude a specific segment from being rebuilt. Otherwise, all segments will be rebuilt.',
|
||||
[]
|
||||
);
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$id = $input->getOption('list-id');
|
||||
$batch = $input->getOption('batch-limit');
|
||||
$max = $input->getOption('max-contacts') ? (int) $input->getOption('max-contacts') : null;
|
||||
$enableTimeMeasurement = (bool) $input->getOption('timing');
|
||||
$output = ($input->getOption('quiet')) ? new NullOutput() : $output;
|
||||
$excludeSegments = $input->getOption('exclude');
|
||||
|
||||
if (!$this->checkRunStatus($input, $output, $id)) {
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
if ($enableTimeMeasurement) {
|
||||
$startTime = microtime(true);
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$list = $this->listModel->getEntity($id);
|
||||
|
||||
if (!$list) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.list.rebuild.not_found', ['%id%' => $id]).'</error>');
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::FAILURE;
|
||||
}
|
||||
|
||||
// Track already rebuilt lists to avoid rebuilding them multiple times
|
||||
$rebuiltLists = [];
|
||||
|
||||
// First check if this segment has dependencies and rebuild them
|
||||
if ($list->hasFilterTypeOf('leadlist')) {
|
||||
$this->rebuildDependentSegments($list, $rebuiltLists, $batch, $max, $output, $enableTimeMeasurement, [], $excludeSegments);
|
||||
}
|
||||
|
||||
// Add the current list ID to the rebuilt lists to avoid rebuilding it again
|
||||
$rebuiltLists[] = (int) $list->getId();
|
||||
|
||||
$this->rebuildSegment($list, $batch, $max, $output, $enableTimeMeasurement);
|
||||
} else {
|
||||
$filter = [
|
||||
'iterable_mode' => true,
|
||||
];
|
||||
|
||||
if (is_array($excludeSegments) && count($excludeSegments) > 0) {
|
||||
$filter['filter'] = [
|
||||
'force' => [
|
||||
[
|
||||
'expr' => 'notIn',
|
||||
'column' => $this->listModel->getRepository()->getTableAlias().'.id',
|
||||
'value' => $excludeSegments,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$rebuiltLists = [];
|
||||
$leadLists = $this->listModel->getEntities($filter);
|
||||
|
||||
/** @var LeadList $leadList */
|
||||
foreach ($leadLists as $leadList) {
|
||||
$listId = $leadList->getId();
|
||||
|
||||
// Skip if already rebuilt
|
||||
if (in_array($listId, $rebuiltLists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process any dependent segments first (segments that are used as filters in this segment)
|
||||
if ($leadList->hasFilterTypeOf('leadlist')) {
|
||||
$this->rebuildDependentSegments($leadList, $rebuiltLists, $batch, $max, $output, $enableTimeMeasurement, [], $excludeSegments);
|
||||
}
|
||||
|
||||
// Add the current list ID to the rebuilt lists to avoid rebuilding it again
|
||||
$rebuiltLists[] = $listId;
|
||||
|
||||
// Rebuild the current segment
|
||||
$this->rebuildSegment($leadList, $batch, $max, $output, $enableTimeMeasurement);
|
||||
}
|
||||
}
|
||||
|
||||
$this->completeRun();
|
||||
|
||||
if ($enableTimeMeasurement) {
|
||||
$totalTime = round(microtime(true) - $startTime, 2);
|
||||
$output->writeln('<fg=magenta>'.$this->translator->trans('mautic.lead.list.rebuild.total.time', ['%time%' => $totalTime]).'</>'."\n");
|
||||
}
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $rebuiltLists List of segment IDs that have already been rebuilt
|
||||
* @param array<int> $dependencyChain Chain of segment IDs to detect circular dependencies
|
||||
* @param array<int|string> $excludeSegments List of segment IDs to exclude from rebuilding
|
||||
*
|
||||
* @param-out array<int> $rebuiltLists Updated list of segment IDs that have been rebuilt
|
||||
*/
|
||||
private function rebuildDependentSegments(
|
||||
LeadList $leadList,
|
||||
array &$rebuiltLists,
|
||||
int $batch,
|
||||
?int $max,
|
||||
OutputInterface $output,
|
||||
bool $enableTimeMeasurement,
|
||||
array $dependencyChain = [],
|
||||
array $excludeSegments = [],
|
||||
): void {
|
||||
// Track the current segment in our dependency chain
|
||||
$currentId = $leadList->getId();
|
||||
$dependencyChain[] = $currentId;
|
||||
|
||||
foreach ($leadList->getFilters() as $filter) {
|
||||
if ('leadlist' === $filter['type']) {
|
||||
foreach ($filter['filter'] ?? [] as $dependentListId) {
|
||||
$dependentListId = (int) $dependentListId;
|
||||
|
||||
// Skip if already rebuilt or in exclude list
|
||||
if (in_array($dependentListId, $rebuiltLists) || in_array($dependentListId, $excludeSegments)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for circular dependency
|
||||
if (in_array($dependentListId, $dependencyChain)) {
|
||||
$output->writeln(
|
||||
'<error>'.$this->translator->trans(
|
||||
'Circular dependency detected in segment chain: %chain%',
|
||||
['%chain%' => implode(' → ', array_merge($dependencyChain, [$dependentListId]))]
|
||||
).'</error>'
|
||||
);
|
||||
continue; // Skip this dependency to prevent infinite recursion
|
||||
}
|
||||
|
||||
$dependentLeadList = $this->listModel->getEntity($dependentListId);
|
||||
if (!$dependentLeadList) {
|
||||
continue; // Skip if the dependent segment doesn't exist - it may have been deleted
|
||||
}
|
||||
|
||||
// Check if this dependent segment has its own dependencies
|
||||
if ($dependentLeadList->hasFilterTypeOf('leadlist')) {
|
||||
// Recursively process this segment's dependencies first, passing the current chain
|
||||
$this->rebuildDependentSegments(
|
||||
$dependentLeadList,
|
||||
$rebuiltLists,
|
||||
$batch,
|
||||
$max,
|
||||
$output,
|
||||
$enableTimeMeasurement,
|
||||
$dependencyChain,
|
||||
$excludeSegments
|
||||
);
|
||||
}
|
||||
|
||||
// Now rebuild this dependent segment
|
||||
$this->rebuildSegment($dependentLeadList, $batch, $max, $output, $enableTimeMeasurement);
|
||||
$rebuiltLists[] = $dependentListId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function rebuildSegment(LeadList $segment, int $batch, ?int $max, OutputInterface $output, bool $enableTimeMeasurement = false): void
|
||||
{
|
||||
if (!$segment->isPublished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln('<info>'.$this->translator->trans('mautic.lead.list.rebuild.rebuilding', ['%id%' => $segment->getId()]).'</info>');
|
||||
$startTime = microtime(true);
|
||||
$processed = $this->listModel->rebuildListLeads($segment, $batch, $max, $output);
|
||||
$rebuildTime = round(microtime(true) - $startTime, 2);
|
||||
|
||||
if (0 >= (int) $max) {
|
||||
// Only full segment rebuilds count
|
||||
$segment->setLastBuiltDateToCurrentDatetime();
|
||||
$segment->setLastBuiltTime($rebuildTime);
|
||||
$this->listModel->saveEntity($segment);
|
||||
}
|
||||
|
||||
$output->writeln(
|
||||
'<comment>'.$this->translator->trans('mautic.lead.list.rebuild.leads_affected', ['%leads%' => $processed]).'</comment>'
|
||||
);
|
||||
|
||||
if ($enableTimeMeasurement) {
|
||||
$output->writeln('<fg=cyan>'.$this->translator->trans(
|
||||
'mautic.lead.list.rebuild.contacts.time',
|
||||
['%time%' => $rebuildTime]
|
||||
).'</>'."\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,918 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_plugin_timeline_index' => [
|
||||
'path' => '/plugin/{integration}/timeline/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\TimelineController::pluginIndexAction',
|
||||
'requirements' => [
|
||||
'integration' => '.+',
|
||||
],
|
||||
],
|
||||
'mautic_plugin_timeline_view' => [
|
||||
'path' => '/plugin/{integration}/timeline/view/{leadId}/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\TimelineController::pluginViewAction',
|
||||
'requirements' => [
|
||||
'integration' => '.+',
|
||||
'leadId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_segment_batch_contact_set' => [
|
||||
'path' => '/segments/batch/contact/set',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\BatchSegmentController::setAction',
|
||||
],
|
||||
'mautic_segment_batch_contact_view' => [
|
||||
'path' => '/segments/batch/contact/view',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\BatchSegmentController::indexAction',
|
||||
],
|
||||
'mautic_segment_index' => [
|
||||
'path' => '/segments/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\ListController::indexAction',
|
||||
],
|
||||
'mautic_segment_action' => [
|
||||
'path' => '/segments/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\ListController::executeAction',
|
||||
],
|
||||
'mautic_contactfield_index' => [
|
||||
'path' => '/contacts/fields/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
],
|
||||
'mautic_contactfield_action' => [
|
||||
'path' => '/contacts/fields/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\FieldController::executeAction',
|
||||
],
|
||||
'mautic_contact_index' => [
|
||||
'path' => '/contacts/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\LeadController::indexAction',
|
||||
],
|
||||
'mautic_contactnote_index' => [
|
||||
'path' => '/contacts/notes/{leadId}/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\NoteController::indexAction',
|
||||
'defaults' => [
|
||||
'leadId' => 0,
|
||||
],
|
||||
'requirements' => [
|
||||
'leadId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_contactnote_action' => [
|
||||
'path' => '/contacts/notes/{leadId}/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\NoteController::executeNoteAction',
|
||||
'requirements' => [
|
||||
'leadId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_contacttimeline_action' => [
|
||||
'path' => '/contacts/timeline/{leadId}/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\TimelineController::indexAction',
|
||||
'requirements' => [
|
||||
'leadId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_contact_timeline_export_action' => [
|
||||
'path' => '/contacts/timeline/batchExport/{leadId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\TimelineController::batchExportAction',
|
||||
'requirements' => [
|
||||
'leadId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_contact_auditlog_action' => [
|
||||
'path' => '/contacts/auditlog/{leadId}/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\AuditlogController::indexAction',
|
||||
'requirements' => [
|
||||
'leadId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_contact_auditlog_export_action' => [
|
||||
'path' => '/contacts/auditlog/batchExport/{leadId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\AuditlogController::batchExportAction',
|
||||
'requirements' => [
|
||||
'leadId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_contact_export_action' => [
|
||||
'path' => '/contacts/contact/export/{contactId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\LeadController::contactExportAction',
|
||||
'requirements' => [
|
||||
'contactId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_import_index' => [
|
||||
'path' => '/{object}/import/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\ImportController::indexAction',
|
||||
],
|
||||
'mautic_import_action' => [
|
||||
'path' => '/{object}/import/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\ImportController::executeAction',
|
||||
],
|
||||
'mautic_contact_action' => [
|
||||
'path' => '/contacts/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\LeadController::executeAction',
|
||||
],
|
||||
'mautic_company_index' => [
|
||||
'path' => '/companies/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\CompanyController::indexAction',
|
||||
],
|
||||
'mautic_company_contacts_list' => [
|
||||
'path' => '/company/{objectId}/contacts/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\CompanyController::contactsListAction',
|
||||
'requirements' => [
|
||||
'objectId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_company_action' => [
|
||||
'path' => '/companies/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\CompanyController::executeAction',
|
||||
],
|
||||
'mautic_company_export_action' => [
|
||||
'path' => '/companies/company/export/{companyId}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\CompanyController::companyExportAction',
|
||||
'requirements' => [
|
||||
'companyId' => '\d+',
|
||||
],
|
||||
],
|
||||
'mautic_segment_contacts' => [
|
||||
'path' => '/segment/view/{objectId}/contact/{page}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\ListController::contactsAction',
|
||||
],
|
||||
'mautic_contact_stats' => [
|
||||
'path' => '/contacts/view/{objectId}/stats',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\LeadController::contactStatsAction',
|
||||
],
|
||||
'mautic_contact_export_download' => [
|
||||
'path' => '/contacts/export/download/{fileName}',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\LeadController::downloadExportAction',
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_api_contactsstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'contacts',
|
||||
'path' => '/contacts',
|
||||
'controller' => Mautic\LeadBundle\Controller\Api\LeadApiController::class,
|
||||
],
|
||||
'mautic_api_dncaddcontact' => [
|
||||
'path' => '/contacts/{id}/dnc/{channel}/add',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::addDncAction',
|
||||
'method' => 'POST',
|
||||
'defaults' => [
|
||||
'channel' => 'email',
|
||||
],
|
||||
],
|
||||
'mautic_api_dncremovecontact' => [
|
||||
'path' => '/contacts/{id}/dnc/{channel}/remove',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::removeDncAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_getcontactevents' => [
|
||||
'path' => '/contacts/{id}/activity',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getActivityAction',
|
||||
],
|
||||
'mautic_api_getcontactsevents' => [
|
||||
'path' => '/contacts/activity',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getAllActivityAction',
|
||||
],
|
||||
'mautic_api_getcontactnotes' => [
|
||||
'path' => '/contacts/{id}/notes',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getNotesAction',
|
||||
],
|
||||
'mautic_api_getcontactdevices' => [
|
||||
'path' => '/contacts/{id}/devices',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getDevicesAction',
|
||||
],
|
||||
'mautic_api_getcontactcampaigns' => [
|
||||
'path' => '/contacts/{id}/campaigns',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getCampaignsAction',
|
||||
],
|
||||
'mautic_api_getcontactssegments' => [
|
||||
'path' => '/contacts/{id}/segments',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getListsAction',
|
||||
],
|
||||
'mautic_api_getcontactscompanies' => [
|
||||
'path' => '/contacts/{id}/companies',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getCompaniesAction',
|
||||
],
|
||||
'mautic_api_utmcreateevent' => [
|
||||
'path' => '/contacts/{id}/utm/add',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::addUtmTagsAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_utmremoveevent' => [
|
||||
'path' => '/contacts/{id}/utm/{utmid}/remove',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::removeUtmTagsAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_getcontactowners' => [
|
||||
'path' => '/contacts/list/owners',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getOwnersAction',
|
||||
],
|
||||
'mautic_api_getcontactfields' => [
|
||||
'path' => '/contacts/list/fields',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\LeadApiController::getFieldsAction',
|
||||
],
|
||||
'mautic_api_getcontactsegments' => [
|
||||
'path' => '/contacts/list/segments',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\ListApiController::getListsAction',
|
||||
],
|
||||
'mautic_api_segmentsstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'lists',
|
||||
'path' => '/segments',
|
||||
'controller' => Mautic\LeadBundle\Controller\Api\ListApiController::class,
|
||||
],
|
||||
'mautic_api_segmentaddcontact' => [
|
||||
'path' => '/segments/{id}/contact/{leadId}/add',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\ListApiController::addLeadAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_segmentaddcontacts' => [
|
||||
'path' => '/segments/{id}/contacts/add',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\ListApiController::addLeadsAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_segmentremovecontact' => [
|
||||
'path' => '/segments/{id}/contact/{leadId}/remove',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\ListApiController::removeLeadAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_companiesstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'companies',
|
||||
'path' => '/companies',
|
||||
'controller' => Mautic\LeadBundle\Controller\Api\CompanyApiController::class,
|
||||
],
|
||||
'mautic_api_companyaddcontact' => [
|
||||
'path' => '/companies/{companyId}/contact/{contactId}/add',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\CompanyApiController::addContactAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_companyremovecontact' => [
|
||||
'path' => '/companies/{companyId}/contact/{contactId}/remove',
|
||||
'controller' => 'Mautic\LeadBundle\Controller\Api\CompanyApiController::removeContactAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_fieldsstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'fields',
|
||||
'path' => '/fields/{object}',
|
||||
'controller' => Mautic\LeadBundle\Controller\Api\FieldApiController::class,
|
||||
'defaults' => [
|
||||
'object' => 'contact',
|
||||
],
|
||||
],
|
||||
'mautic_api_notesstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'notes',
|
||||
'path' => '/notes',
|
||||
'controller' => Mautic\LeadBundle\Controller\Api\NoteApiController::class,
|
||||
],
|
||||
'mautic_api_devicesstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'devices',
|
||||
'path' => '/devices',
|
||||
'controller' => Mautic\LeadBundle\Controller\Api\DeviceApiController::class,
|
||||
],
|
||||
'mautic_api_tagsstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'tags',
|
||||
'path' => '/tags',
|
||||
'controller' => Mautic\LeadBundle\Controller\Api\TagApiController::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'items' => [
|
||||
'mautic.lead.leads' => [
|
||||
'iconClass' => 'ri-user-6-fill',
|
||||
'access' => ['lead:leads:viewown', 'lead:leads:viewother'],
|
||||
'route' => 'mautic_contact_index',
|
||||
'priority' => 80,
|
||||
],
|
||||
'mautic.companies.menu.index' => [
|
||||
'route' => 'mautic_company_index',
|
||||
'iconClass' => 'ri-building-2-fill',
|
||||
'access' => ['lead:leads:viewother'],
|
||||
'priority' => 75,
|
||||
],
|
||||
'mautic.lead.list.menu.index' => [
|
||||
'iconClass' => 'ri-pie-chart-fill',
|
||||
'access' => ['lead:lists:viewown', 'lead:lists:viewother'],
|
||||
'route' => 'mautic_segment_index',
|
||||
'priority' => 70,
|
||||
],
|
||||
],
|
||||
],
|
||||
'admin' => [
|
||||
'priority' => 50,
|
||||
'items' => [
|
||||
'mautic.lead.field.menu.index' => [
|
||||
'id' => 'mautic_lead_field',
|
||||
'iconClass' => 'ri-input-field',
|
||||
'route' => 'mautic_contactfield_index',
|
||||
'access' => 'lead:fields:full',
|
||||
'priority' => 19,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'categories' => [
|
||||
'segment' => [
|
||||
'class' => Mautic\LeadBundle\Entity\LeadList::class,
|
||||
],
|
||||
],
|
||||
'services' => [
|
||||
'events' => [
|
||||
'mautic.lead.serializer.subscriber' => [
|
||||
'class' => Mautic\LeadBundle\EventListener\SerializerSubscriber::class,
|
||||
'arguments' => [
|
||||
'request_stack',
|
||||
],
|
||||
'tag' => 'jms_serializer.event_subscriber',
|
||||
'tagArguments' => [
|
||||
'event' => JMS\Serializer\EventDispatcher\Events::POST_SERIALIZE,
|
||||
],
|
||||
],
|
||||
'mautic.lead.export_scheduled_audit_log_subscriber' => [
|
||||
'class' => Mautic\LeadBundle\EventListener\ContactExportSchedulerAuditLogSubscriber::class,
|
||||
'arguments' => [
|
||||
'mautic.core.model.auditlog',
|
||||
'mautic.helper.ip_lookup',
|
||||
],
|
||||
],
|
||||
'mautic.lead.export_scheduled_logger_subscriber' => [
|
||||
'class' => Mautic\LeadBundle\EventListener\ContactExportSchedulerLoggerSubscriber::class,
|
||||
'arguments' => [
|
||||
'monolog.logger.mautic',
|
||||
],
|
||||
],
|
||||
'mautic.lead.export_scheduled_notification_subscriber' => [
|
||||
'class' => Mautic\LeadBundle\EventListener\ContactExportSchedulerNotificationSubscriber::class,
|
||||
'arguments' => [
|
||||
'mautic.core.model.notification',
|
||||
'translator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.contact_scheduled_export.subscriber' => [
|
||||
'class' => Mautic\LeadBundle\EventListener\ContactScheduledExportSubscriber::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.export_scheduler',
|
||||
],
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
'mautic.validator.leadlistaccess' => [
|
||||
'class' => Mautic\LeadBundle\Form\Validator\Constraints\LeadListAccessValidator::class,
|
||||
'arguments' => ['mautic.lead.model.list'],
|
||||
'tag' => 'validator.constraint_validator',
|
||||
'alias' => 'leadlist_access',
|
||||
],
|
||||
'mautic.validator.emailaddress' => [
|
||||
'class' => Mautic\LeadBundle\Form\Validator\Constraints\EmailAddressValidator::class,
|
||||
'arguments' => [
|
||||
'mautic.validator.email',
|
||||
],
|
||||
'tag' => 'validator.constraint_validator',
|
||||
],
|
||||
Mautic\LeadBundle\Form\Validator\Constraints\FieldAliasKeywordValidator::class => [
|
||||
'class' => Mautic\LeadBundle\Form\Validator\Constraints\FieldAliasKeywordValidator::class,
|
||||
'tag' => 'validator.constraint_validator',
|
||||
'arguments' => [
|
||||
'mautic.lead.model.list',
|
||||
'mautic.helper.field.alias',
|
||||
'@doctrine.orm.entity_manager',
|
||||
'translator',
|
||||
'mautic.lead.repository.lead_segment_filter_descriptor',
|
||||
],
|
||||
],
|
||||
Mautic\CoreBundle\Form\Validator\Constraints\FileEncodingValidator::class => [
|
||||
'class' => Mautic\CoreBundle\Form\Validator\Constraints\FileEncodingValidator::class,
|
||||
'tag' => 'validator.constraint_validator',
|
||||
'arguments' => [
|
||||
'mautic.lead.model.list',
|
||||
'mautic.helper.field.alias',
|
||||
],
|
||||
],
|
||||
'mautic.lead.constraint.alias' => [
|
||||
'class' => Mautic\LeadBundle\Form\Validator\Constraints\UniqueUserAliasValidator::class,
|
||||
'arguments' => ['mautic.lead.repository.lead_list', 'mautic.helper.user'],
|
||||
'tag' => 'validator.constraint_validator',
|
||||
'alias' => 'uniqueleadlist',
|
||||
],
|
||||
'mautic.lead.validator.custom_field' => [
|
||||
'class' => Mautic\LeadBundle\Validator\CustomFieldValidator::class,
|
||||
'arguments' => ['mautic.lead.model.field', 'translator'],
|
||||
],
|
||||
'mautic.lead_list.constraint.in_use' => [
|
||||
'class' => Mautic\LeadBundle\Form\Validator\Constraints\SegmentInUseValidator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.list',
|
||||
],
|
||||
'tag' => 'validator.constraint_validator',
|
||||
'alias' => 'segment_in_use',
|
||||
],
|
||||
'mautic.lead.validator.lead.list.campaign' => [
|
||||
'class' => Mautic\LeadBundle\Validator\SegmentUsedInCampaignsValidator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.repository.lead_list',
|
||||
'translator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.constraint.validator.lead.list.campaign' => [
|
||||
'class' => Mautic\LeadBundle\Validator\Constraints\SegmentUsedInCampaignsValidator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.validator.lead.list.campaign',
|
||||
],
|
||||
'tag' => 'validator.constraint_validator',
|
||||
],
|
||||
'mautic.lead.event.dispatcher' => [
|
||||
'class' => Mautic\LeadBundle\Helper\LeadChangeEventDispatcher::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.merger' => [
|
||||
'class' => Mautic\LeadBundle\Deduplicate\ContactMerger::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead',
|
||||
'mautic.lead.repository.merged_records',
|
||||
'event_dispatcher',
|
||||
'monolog.logger.mautic',
|
||||
],
|
||||
],
|
||||
'mautic.lead.deduper' => [
|
||||
'class' => Mautic\LeadBundle\Deduplicate\ContactDeduper::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.field',
|
||||
'mautic.lead.field.fields_with_unique_identifier',
|
||||
'mautic.lead.merger',
|
||||
'mautic.lead.repository.lead',
|
||||
],
|
||||
],
|
||||
'mautic.lead.helper.primary_company' => [
|
||||
'class' => Mautic\LeadBundle\Helper\PrimaryCompanyHelper::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.repository.company_lead',
|
||||
],
|
||||
],
|
||||
'mautic.lead.validator.length' => [
|
||||
'class' => Mautic\LeadBundle\Validator\Constraints\LengthValidator::class,
|
||||
'tag' => 'validator.constraint_validator',
|
||||
],
|
||||
'mautic.lead.segment.stat.dependencies' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Stat\SegmentDependencies::class,
|
||||
'arguments' => [
|
||||
'mautic.email.model.email',
|
||||
'mautic.campaign.model.campaign',
|
||||
'mautic.form.model.action',
|
||||
'mautic.lead.model.list',
|
||||
'mautic.point.model.triggerevent',
|
||||
'mautic.report.model.report',
|
||||
],
|
||||
],
|
||||
'mautic.lead.segment.stat.chart.query.factory' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Stat\SegmentChartQueryFactory::class,
|
||||
'arguments' => [
|
||||
],
|
||||
],
|
||||
'mautic.lead.segment.stat.campaign.share' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Stat\SegmentCampaignShare::class,
|
||||
'arguments' => [
|
||||
'mautic.campaign.model.campaign',
|
||||
'mautic.helper.cache_storage',
|
||||
'@doctrine.orm.entity_manager',
|
||||
],
|
||||
],
|
||||
'mautic.lead.columns.dictionary' => [
|
||||
'class' => Mautic\LeadBundle\Services\ContactColumnsDictionary::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.field',
|
||||
'translator',
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_filter_factory' => [
|
||||
'class' => Mautic\LeadBundle\Segment\ContactSegmentFilterFactory::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead_segment_schema_cache',
|
||||
'@service_container',
|
||||
'mautic.lead.model.lead_segment_decorator_factory',
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.tracker.device' => [
|
||||
'class' => Mautic\LeadBundle\Tracker\DeviceTracker::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.service.device_creator_service',
|
||||
'mautic.lead.factory.device_detector_factory',
|
||||
'mautic.lead.service.device_tracking_service',
|
||||
'monolog.logger.mautic',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.custom_field_column' => [
|
||||
'class' => Mautic\LeadBundle\Field\CustomFieldColumn::class,
|
||||
'arguments' => [
|
||||
'mautic.schema.helper.column',
|
||||
'mautic.lead.field.schema_definition',
|
||||
'monolog.logger.mautic',
|
||||
'mautic.lead.field.lead_field_saver',
|
||||
'mautic.lead.field.custom_field_index',
|
||||
'mautic.lead.field.dispatcher.field_column_dispatcher',
|
||||
'translator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.custom_field_index' => [
|
||||
'class' => Mautic\LeadBundle\Field\CustomFieldIndex::class,
|
||||
'arguments' => [
|
||||
'mautic.schema.helper.index',
|
||||
'monolog.logger.mautic',
|
||||
'mautic.lead.field.fields_with_unique_identifier',
|
||||
],
|
||||
],
|
||||
'mautic.lead.repository.lead_segment_filter_descriptor' => [
|
||||
'class' => Mautic\LeadBundle\Services\ContactSegmentFilterDictionary::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.service.segment_dependency_tree_factory' => [
|
||||
'class' => Mautic\LeadBundle\Services\SegmentDependencyTreeFactory::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.list',
|
||||
'router',
|
||||
],
|
||||
],
|
||||
'mautic.lead.repository.lead_segment_query_builder' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder::class,
|
||||
'arguments' => [
|
||||
'doctrine.orm.entity_manager',
|
||||
'mautic.lead.model.random_parameter_name',
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_service' => [
|
||||
'class' => Mautic\LeadBundle\Segment\ContactSegmentService::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead_segment_filter_factory',
|
||||
'mautic.lead.repository.lead_segment_query_builder',
|
||||
'monolog.logger.mautic',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_schema_cache' => [
|
||||
'class' => Mautic\LeadBundle\Segment\TableSchemaColumnsCache::class,
|
||||
'arguments' => [
|
||||
'doctrine.orm.entity_manager',
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.relative_date' => [
|
||||
'class' => Mautic\LeadBundle\Segment\RelativeDate::class,
|
||||
'arguments' => [
|
||||
'translator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_filter_operator' => [
|
||||
'class' => Mautic\LeadBundle\Segment\ContactSegmentFilterOperator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.provider.fillterOperator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_decorator_factory' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Decorator\DecoratorFactory::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.repository.lead_segment_filter_descriptor',
|
||||
'mautic.lead.model.lead_segment_decorator_base',
|
||||
'mautic.lead.model.lead_segment_decorator_custom_mapped',
|
||||
'mautic.lead.model.lead_segment.decorator.date.optionFactory',
|
||||
'mautic.lead.model.lead_segment_decorator_company',
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_decorator_base' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Decorator\BaseDecorator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead_segment_filter_operator',
|
||||
'mautic.lead.repository.lead_segment_filter_descriptor',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_decorator_custom_mapped' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead_segment_filter_operator',
|
||||
'mautic.lead.repository.lead_segment_filter_descriptor',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_decorator_company' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Decorator\CompanyDecorator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead_segment_filter_operator',
|
||||
'mautic.lead.repository.lead_segment_filter_descriptor',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment_decorator_date' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Decorator\DateDecorator::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead_segment_filter_operator',
|
||||
'mautic.lead.repository.lead_segment_filter_descriptor',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment.decorator.date.optionFactory' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.lead_segment_decorator_date',
|
||||
'mautic.lead.model.relative_date',
|
||||
'mautic.lead.model.lead_segment.timezoneResolver',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.lead_segment.timezoneResolver' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Decorator\Date\TimezoneResolver::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
],
|
||||
'mautic.lead.provider.fillterOperator' => [
|
||||
'class' => Mautic\LeadBundle\Provider\FilterOperatorProvider::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'translator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.provider.typeOperator' => [
|
||||
'class' => Mautic\LeadBundle\Provider\TypeOperatorProvider::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'mautic.lead.provider.fillterOperator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.provider.fieldChoices' => [
|
||||
'class' => Mautic\LeadBundle\Provider\FieldChoicesProvider::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.provider.formAdjustments' => [
|
||||
'class' => Mautic\LeadBundle\Provider\FormAdjustmentsProvider::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.model.random_parameter_name' => [
|
||||
'class' => Mautic\LeadBundle\Segment\RandomParameterName::class,
|
||||
],
|
||||
'mautic.lead.segment.operator_options' => [
|
||||
'class' => Mautic\LeadBundle\Segment\OperatorOptions::class,
|
||||
],
|
||||
'mautic.lead.reportbundle.fields_builder' => [
|
||||
'class' => Mautic\LeadBundle\Report\FieldsBuilder::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.field',
|
||||
'mautic.lead.model.list',
|
||||
'mautic.user.model.user',
|
||||
'mautic.lead.model.lead',
|
||||
'mautic.lead.report.dnc_report_service',
|
||||
],
|
||||
],
|
||||
'mautic.lead.factory.device_detector_factory' => [
|
||||
'class' => Mautic\LeadBundle\Tracker\Factory\DeviceDetectorFactory\DeviceDetectorFactory::class,
|
||||
'arguments' => [
|
||||
'mautic.cache.provider',
|
||||
],
|
||||
],
|
||||
'mautic.lead.service.contact_tracking_service' => [
|
||||
'class' => Mautic\LeadBundle\Tracker\Service\ContactTrackingService\ContactTrackingService::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.cookie',
|
||||
'mautic.lead.repository.lead_device',
|
||||
'mautic.lead.repository.lead',
|
||||
'mautic.lead.repository.merged_records',
|
||||
'request_stack',
|
||||
],
|
||||
],
|
||||
'mautic.lead.service.device_creator_service' => [
|
||||
'class' => Mautic\LeadBundle\Tracker\Service\DeviceCreatorService\DeviceCreatorService::class,
|
||||
],
|
||||
'mautic.lead.service.device_tracking_service' => [
|
||||
'class' => Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingService::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.cookie',
|
||||
'doctrine.orm.entity_manager',
|
||||
'mautic.lead.repository.lead_device',
|
||||
'mautic.helper.random',
|
||||
'request_stack',
|
||||
'mautic.security',
|
||||
],
|
||||
],
|
||||
|
||||
'mautic.lead.field.schema_definition' => [
|
||||
'class' => Mautic\LeadBundle\Field\SchemaDefinition::class,
|
||||
],
|
||||
'mautic.lead.field.dispatcher.field_save_dispatcher' => [
|
||||
'class' => Mautic\LeadBundle\Field\Dispatcher\FieldSaveDispatcher::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'doctrine.orm.entity_manager',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.dispatcher.field_column_dispatcher' => [
|
||||
'class' => Mautic\LeadBundle\Field\Dispatcher\FieldColumnDispatcher::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'mautic.lead.field.settings.background_settings',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.dispatcher.field_column_background_dispatcher' => [
|
||||
'class' => Mautic\LeadBundle\Field\Dispatcher\FieldColumnBackgroundJobDispatcher::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.fields_with_unique_identifier' => [
|
||||
'class' => Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.field.field_list',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.field_list' => [
|
||||
'class' => Mautic\LeadBundle\Field\FieldList::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.repository.field',
|
||||
'translator',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.identifier_fields' => [
|
||||
'class' => Mautic\LeadBundle\Field\IdentifierFields::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.field.fields_with_unique_identifier',
|
||||
'mautic.lead.field.field_list',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.lead_field_saver' => [
|
||||
'class' => Mautic\LeadBundle\Field\LeadFieldSaver::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.repository.field',
|
||||
'mautic.lead.field.dispatcher.field_save_dispatcher',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.settings.background_settings' => [
|
||||
'class' => Mautic\LeadBundle\Field\Settings\BackgroundSettings::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
],
|
||||
'mautic.lead.field.notification.custom_field' => [
|
||||
'class' => Mautic\LeadBundle\Field\Notification\CustomFieldNotification::class,
|
||||
'arguments' => [
|
||||
'mautic.core.model.notification',
|
||||
'mautic.user.model.user',
|
||||
'translator',
|
||||
],
|
||||
],
|
||||
|
||||
// Segment Filter Query builders
|
||||
'mautic.lead.query.builder.basic' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\BaseFilterQueryBuilder::class,
|
||||
'arguments' => ['mautic.lead.model.random_parameter_name', 'event_dispatcher'],
|
||||
],
|
||||
'mautic.lead.query.builder.foreign.value' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\ForeignValueFilterQueryBuilder::class,
|
||||
'arguments' => ['mautic.lead.model.random_parameter_name', 'event_dispatcher'],
|
||||
],
|
||||
'mautic.lead.query.builder.foreign.func' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\ForeignFuncFilterQueryBuilder::class,
|
||||
'arguments' => ['mautic.lead.model.random_parameter_name', 'event_dispatcher'],
|
||||
],
|
||||
'mautic.lead.query.builder.special.dnc' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\DoNotContactFilterQueryBuilder::class,
|
||||
'arguments' => ['mautic.lead.model.random_parameter_name', 'event_dispatcher'],
|
||||
],
|
||||
'mautic.lead.query.builder.special.integration' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\IntegrationCampaignFilterQueryBuilder::class,
|
||||
'arguments' => ['mautic.lead.model.random_parameter_name', 'event_dispatcher'],
|
||||
],
|
||||
'mautic.lead.query.builder.special.sessions' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder::class,
|
||||
'arguments' => ['mautic.lead.model.random_parameter_name', 'event_dispatcher'],
|
||||
],
|
||||
'mautic.lead.query.builder.complex_relation.value' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\ComplexRelationValueFilterQueryBuilder::class,
|
||||
'arguments' => ['mautic.lead.model.random_parameter_name', 'event_dispatcher'],
|
||||
],
|
||||
'mautic.lead.query.builder.channel_click.value' => [
|
||||
'class' => Mautic\LeadBundle\Segment\Query\Filter\ChannelClickQueryBuilder::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.model.random_parameter_name',
|
||||
'event_dispatcher',
|
||||
],
|
||||
],
|
||||
],
|
||||
'helpers' => [
|
||||
'mautic.helper.twig.avatar' => [
|
||||
'class' => Mautic\LeadBundle\Twig\Helper\AvatarHelper::class,
|
||||
'arguments' => [
|
||||
'twig.helper.assets',
|
||||
'mautic.helper.paths',
|
||||
'mautic.helper.twig.gravatar',
|
||||
'mautic.helper.twig.default_avatar',
|
||||
],
|
||||
'alias' => 'lead_avatar',
|
||||
],
|
||||
'mautic.helper.twig.default_avatar' => [
|
||||
'class' => Mautic\LeadBundle\Twig\Helper\DefaultAvatarHelper::class,
|
||||
'arguments' => [
|
||||
'twig.helper.assets',
|
||||
],
|
||||
'alias' => 'default_avatar',
|
||||
],
|
||||
'mautic.helper.field.alias' => [
|
||||
'class' => Mautic\LeadBundle\Helper\FieldAliasHelper::class,
|
||||
'arguments' => ['mautic.lead.model.field'],
|
||||
],
|
||||
'mautic.helper.twig.dnc_reason' => [
|
||||
'class' => Mautic\LeadBundle\Twig\Helper\DncReasonHelper::class,
|
||||
'arguments' => ['translator'],
|
||||
'alias' => 'lead_dnc_reason',
|
||||
],
|
||||
],
|
||||
'fixtures' => [
|
||||
'mautic.lead.fixture.company' => [
|
||||
'class' => Mautic\LeadBundle\DataFixtures\ORM\LoadCompanyData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.lead.model.company'],
|
||||
],
|
||||
'mautic.lead.fixture.contact' => [
|
||||
'class' => Mautic\LeadBundle\DataFixtures\ORM\LoadLeadData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.helper.core_parameters'],
|
||||
],
|
||||
'mautic.lead.fixture.segment' => [
|
||||
'class' => Mautic\LeadBundle\DataFixtures\ORM\LoadLeadListData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.lead.model.list'],
|
||||
],
|
||||
'mautic.lead.fixture.category' => [
|
||||
'class' => Mautic\LeadBundle\DataFixtures\ORM\LoadCategoryData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
],
|
||||
'mautic.lead.fixture.categorizedleadlists' => [
|
||||
'class' => Mautic\LeadBundle\DataFixtures\ORM\LoadCategorizedLeadListData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
],
|
||||
'mautic.lead.fixture.test.page_hit' => [
|
||||
'class' => Mautic\LeadBundle\Tests\DataFixtures\ORM\LoadPageHitData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'optional' => true,
|
||||
],
|
||||
'mautic.lead.fixture.test.segment' => [
|
||||
'class' => Mautic\LeadBundle\Tests\DataFixtures\ORM\LoadSegmentsData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.lead.model.list', 'mautic.lead.model.lead'],
|
||||
'optional' => true,
|
||||
],
|
||||
'mautic.lead.fixture.test.click' => [
|
||||
'class' => Mautic\LeadBundle\Tests\DataFixtures\ORM\LoadClickData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.lead.model.list', 'mautic.lead.model.lead'],
|
||||
'optional' => true,
|
||||
],
|
||||
'mautic.lead.fixture.test.dnc' => [
|
||||
'class' => Mautic\LeadBundle\Tests\DataFixtures\ORM\LoadDncData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.lead.model.list', 'mautic.lead.model.lead'],
|
||||
'optional' => true,
|
||||
],
|
||||
'mautic.lead.fixture.test.tag' => [
|
||||
'class' => Mautic\LeadBundle\Tests\DataFixtures\ORM\LoadTagData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'optional' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'parameters' => [
|
||||
'parallel_import_limit' => 1,
|
||||
'background_import_if_more_rows_than' => 0,
|
||||
'contact_api_count_cache_ttl' => 5, // in seconds, set null to disable.
|
||||
'contact_columns' => [
|
||||
'0' => 'name',
|
||||
'1' => 'email',
|
||||
'2' => 'location',
|
||||
'3' => 'stage',
|
||||
'4' => 'points',
|
||||
'5' => 'last_active',
|
||||
'6' => 'id',
|
||||
],
|
||||
Mautic\LeadBundle\Field\Settings\BackgroundSettings::CREATE_CUSTOM_FIELD_IN_BACKGROUND => false,
|
||||
'company_unique_identifiers_operator' => Doctrine\DBAL\Query\Expression\CompositeExpression::TYPE_OR,
|
||||
'contact_unique_identifiers_operator' => Doctrine\DBAL\Query\Expression\CompositeExpression::TYPE_OR,
|
||||
'segment_rebuild_time_warning' => 30,
|
||||
'segment_build_time_warning' => 30,
|
||||
'contact_export_in_background' => true,
|
||||
'contact_export_dir' => '%mautic.application_dir%/media/files/temp',
|
||||
'contact_export_batch_size' => 20000,
|
||||
'contact_export_limit' => 0,
|
||||
'contact_allow_multiple_companies' => true,
|
||||
'import_leads_dir' => '%kernel.project_dir%/var/import',
|
||||
'update_segment_contact_count_in_background' => false,
|
||||
'clear_export_files_after_days' => 7,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
'Deduplicate/Exception',
|
||||
'Field/DTO',
|
||||
'Field/Event',
|
||||
'Segment/ContactSegmentFilter.php',
|
||||
'Segment/ContactSegmentFilterCrate.php',
|
||||
'Segment/Decorator',
|
||||
'Segment/DoNotContact',
|
||||
'Segment/IntegrationCampaign',
|
||||
'Segment/Query',
|
||||
'Segment/Stat',
|
||||
];
|
||||
|
||||
$services->load('Mautic\\LeadBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\LeadBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
$services->alias('mautic.lead.model.lead', Mautic\LeadBundle\Model\LeadModel::class);
|
||||
$services->get(Mautic\LeadBundle\Entity\CompanyRepository::class)
|
||||
->call('setUniqueIdentifiersOperator', ['%mautic.company_unique_identifiers_operator%']);
|
||||
$services->get(Mautic\LeadBundle\Entity\LeadRepository::class)
|
||||
->call('setUniqueIdentifiersOperator', ['%mautic.contact_unique_identifiers_operator%'])
|
||||
->call('setListLeadRepository', [\Symfony\Component\DependencyInjection\Loader\Configurator\service('mautic.lead.repository.list_lead')])
|
||||
->call('setLeadFieldRepository', [\Symfony\Component\DependencyInjection\Loader\Configurator\service('mautic.lead.repository.field')]);
|
||||
|
||||
$services->alias('mautic.lead.model.field', Mautic\LeadBundle\Model\FieldModel::class);
|
||||
$services->alias('mautic.lead.model.list', Mautic\LeadBundle\Model\ListModel::class);
|
||||
$services->alias('mautic.lead.model.note', Mautic\LeadBundle\Model\NoteModel::class);
|
||||
$services->alias('mautic.lead.model.device', Mautic\LeadBundle\Model\DeviceModel::class);
|
||||
$services->alias('mautic.lead.model.company', Mautic\LeadBundle\Model\CompanyModel::class);
|
||||
$services->alias('mautic.lead.model.import', Mautic\LeadBundle\Model\ImportModel::class);
|
||||
$services->alias('mautic.lead.model.tag', Mautic\LeadBundle\Model\TagModel::class);
|
||||
$services->alias('mautic.lead.model.company_report_data', Mautic\LeadBundle\Model\CompanyReportData::class);
|
||||
$services->alias('mautic.lead.model.dnc', Mautic\LeadBundle\Model\DoNotContact::class);
|
||||
$services->alias('mautic.lead.model.segment.action', Mautic\LeadBundle\Model\SegmentActionModel::class);
|
||||
$services->alias('mautic.lead.model.ipaddress', Mautic\LeadBundle\Model\IpAddressModel::class);
|
||||
$services->alias('mautic.lead.model.export_scheduler', Mautic\LeadBundle\Model\ContactExportSchedulerModel::class);
|
||||
$services->alias('mautic.lead.repository.company', Mautic\LeadBundle\Entity\CompanyRepository::class);
|
||||
$services->alias('mautic.lead.repository.company_lead', Mautic\LeadBundle\Entity\CompanyLeadRepository::class);
|
||||
$services->alias('mautic.lead.repository.stages_lead_log', Mautic\LeadBundle\Entity\StagesChangeLogRepository::class);
|
||||
$services->alias('mautic.lead.repository.dnc', Mautic\LeadBundle\Entity\DoNotContactRepository::class);
|
||||
$services->alias('mautic.lead.repository.lead', Mautic\LeadBundle\Entity\LeadRepository::class);
|
||||
$services->alias('mautic.lead.repository.list_lead', Mautic\LeadBundle\Entity\ListLeadRepository::class);
|
||||
$services->alias('mautic.lead.repository.frequency_rule', Mautic\LeadBundle\Entity\FrequencyRuleRepository::class);
|
||||
$services->alias('mautic.lead.repository.lead_event_log', Mautic\LeadBundle\Entity\LeadEventLogRepository::class);
|
||||
$services->alias('mautic.lead.repository.lead_device', Mautic\LeadBundle\Entity\LeadDeviceRepository::class);
|
||||
$services->alias('mautic.lead.repository.lead_list', Mautic\LeadBundle\Entity\LeadListRepository::class);
|
||||
$services->alias('mautic.lead.repository.points_change_log', Mautic\LeadBundle\Entity\PointsChangeLogRepository::class);
|
||||
$services->alias('mautic.lead.repository.merged_records', Mautic\LeadBundle\Entity\MergeRecordRepository::class);
|
||||
$services->alias('mautic.lead.repository.field', Mautic\LeadBundle\Entity\LeadFieldRepository::class);
|
||||
$services->alias('mautic.company.deduper', Mautic\LeadBundle\Deduplicate\CompanyDeduper::class);
|
||||
$services->alias('mautic.lead.helper.contact_request_helper', Mautic\LeadBundle\Helper\ContactRequestHelper::class);
|
||||
$services->alias('mautic.lead.helper.dnc_formatter_helper', Mautic\LeadBundle\Helper\DncFormatterHelper::class);
|
||||
$services->alias('mautic.tracker.contact', Mautic\LeadBundle\Tracker\ContactTracker::class);
|
||||
$services->alias('mautic.lead.field.settings.background_service', Mautic\LeadBundle\Field\BackgroundService::class);
|
||||
$services->alias('mautic.lead.report.dnc_report_service', Mautic\LeadBundle\Report\DncReportService::class);
|
||||
$services->alias('mautic.helper.segment.count.cache', Mautic\LeadBundle\Helper\SegmentCountCacheHelper::class);
|
||||
$services->get(Mautic\LeadBundle\Validator\Constraints\SegmentDateValidator::class)->tag('validator.constraint_validator');
|
||||
};
|
||||
@@ -0,0 +1,845 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Membership\MembershipManager;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\Tree\JsPlumbFormatter;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\UtmTag;
|
||||
use Mautic\LeadBundle\Event\ListTypeaheadEvent;
|
||||
use Mautic\LeadBundle\Form\Type\FieldType;
|
||||
use Mautic\LeadBundle\Form\Type\FilterPropertiesType;
|
||||
use Mautic\LeadBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Provider\FormAdjustmentsProviderInterface;
|
||||
use Mautic\LeadBundle\Segment\SegmentFilterIconTrait;
|
||||
use Mautic\LeadBundle\Segment\Stat\SegmentCampaignShare;
|
||||
use Mautic\LeadBundle\Services\ContactColumnsDictionary;
|
||||
use Mautic\LeadBundle\Services\SegmentDependencyTreeFactory;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
use AjaxLookupControllerTrait;
|
||||
use SegmentFilterIconTrait;
|
||||
|
||||
public function userListAction(Request $request): JsonResponse
|
||||
{
|
||||
$filter = InputHelper::clean($request->query->get('filter'));
|
||||
$leadModel = $this->getModel('lead.lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$results = $leadModel->getLookupResults('user', $filter);
|
||||
$dataArray = [];
|
||||
foreach ($results as $r) {
|
||||
$name = $r['firstName'].' '.$r['lastName'];
|
||||
$dataArray[] = [
|
||||
'label' => $name,
|
||||
'value' => $r['id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function contactListAction(Request $request, LeadModel $model, CorePermissions $corePermissions): JsonResponse
|
||||
{
|
||||
$filter['string'] = InputHelper::clean($request->query->get('filter'));
|
||||
|
||||
// Do not show other's contacts if do not have permission.
|
||||
if (!$corePermissions->isGranted(['lead:leads:viewother'], 'MATCH_ONE')) {
|
||||
$filter['force'] = ' '.$this->translator->trans('mautic.core.searchcommand.ismine');
|
||||
}
|
||||
|
||||
$results = $model->getLookupResults('contact', $filter);
|
||||
|
||||
$results['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($results);
|
||||
}
|
||||
|
||||
public function getLeadIdsByFieldValueAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$field = InputHelper::clean($request->query->get('field'));
|
||||
$value = InputHelper::clean($request->query->all()['value'] ?? '');
|
||||
$ignore = (int) $request->query->get('ignore');
|
||||
$dataArray = ['items' => []];
|
||||
|
||||
if ($field && $value) {
|
||||
$repo = $leadModel->getRepository();
|
||||
$leads = $repo->getLeadsByFieldValue($field, $value, $ignore);
|
||||
$dataArray['existsMessage'] = $this->translator->trans('mautic.lead.exists.by.field').': ';
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$fields = $repo->getFieldValues($lead->getId());
|
||||
$lead->setFields($fields);
|
||||
$name = $lead->getName();
|
||||
|
||||
if (!$name) {
|
||||
$name = $lead->getEmail();
|
||||
}
|
||||
|
||||
if (!$name) {
|
||||
$name = $this->translator->trans('mautic.lead.lead.anonymous');
|
||||
}
|
||||
|
||||
$leadLink = $this->generateUrl('mautic_contact_action', ['objectAction' => 'view', 'objectId' => $lead->getId()]);
|
||||
|
||||
$dataArray['items'][] = [
|
||||
'name' => $name,
|
||||
'id' => $lead->getId(),
|
||||
'link' => $leadLink,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function fieldListAction(Request $request): JsonResponse
|
||||
{
|
||||
$filter = InputHelper::clean($request->query->get('filter'));
|
||||
$fieldAlias = InputHelper::alphanum($request->query->get('field'), false, null, ['_']);
|
||||
$event = new ListTypeaheadEvent($fieldAlias, $filter);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $this->sendJsonResponse($event->getDataArray());
|
||||
}
|
||||
|
||||
public function loadSegmentFilterFormAction(
|
||||
Request $request,
|
||||
FormFactoryInterface $formFactory,
|
||||
FormAdjustmentsProviderInterface $formAdjustmentsProvider,
|
||||
ListModel $listModel,
|
||||
): JsonResponse {
|
||||
$fieldAlias = InputHelper::clean($request->request->get('fieldAlias'));
|
||||
$fieldObject = InputHelper::clean($request->request->get('fieldObject'));
|
||||
$operator = InputHelper::clean($request->request->get('operator'));
|
||||
$search = InputHelper::clean($request->request->get('search'));
|
||||
$filterNum = (int) $request->request->get('filterNum');
|
||||
|
||||
$form = $formFactory->createNamed('RENAME', FilterPropertiesType::class);
|
||||
|
||||
if ($fieldAlias && $operator) {
|
||||
$formAdjustmentsProvider->adjustForm(
|
||||
$form,
|
||||
$fieldAlias,
|
||||
$fieldObject,
|
||||
$operator,
|
||||
$listModel->getChoiceFields($search)[$fieldObject][$fieldAlias]
|
||||
);
|
||||
}
|
||||
|
||||
$formHtml = $this->renderView(
|
||||
'@MauticLead/List/filterpropform.html.twig',
|
||||
[
|
||||
// 'form' => $this->setFormTheme($form, '@MauticLead/List/filterpropform.html.twig', []),
|
||||
'form' => $form->createView(),
|
||||
]
|
||||
);
|
||||
|
||||
$formHtml = str_replace('id="RENAME', "id=\"leadlist_filters_{$filterNum}_properties", $formHtml);
|
||||
$formHtml = str_replace('name="RENAME', "name=\"leadlist[filters][{$filterNum}][properties]", $formHtml);
|
||||
|
||||
return $this->sendJsonResponse(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $formHtml,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cache and gets returns updated HTML.
|
||||
*/
|
||||
public function updateSocialProfileAction(Request $request, IntegrationHelper $integrationHelper): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$network = InputHelper::clean($request->request->get('network'));
|
||||
$leadId = InputHelper::clean($request->request->get('lead'));
|
||||
|
||||
if (!empty($leadId)) {
|
||||
// find the lead
|
||||
$model = $this->getModel('lead.lead');
|
||||
$lead = $model->getEntity($leadId);
|
||||
|
||||
if (null !== $lead && $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editown', $lead->getPermissionUser())) {
|
||||
$leadFields = $lead->getFields();
|
||||
$socialProfiles = $integrationHelper->getUserProfiles($lead, $leadFields, true, $network);
|
||||
$socialProfileUrls = $integrationHelper->getSocialProfileUrlRegex(false);
|
||||
$integrations = [];
|
||||
$socialCount = count($socialProfiles);
|
||||
if (empty($network) || empty($socialCount)) {
|
||||
$dataArray['completeProfile'] = $this->renderView(
|
||||
'@MauticLead/Social/index.html.twig',
|
||||
[
|
||||
'socialProfiles' => $socialProfiles,
|
||||
'lead' => $lead,
|
||||
'socialProfileUrls' => $socialProfileUrls,
|
||||
]
|
||||
);
|
||||
$dataArray['socialCount'] = $socialCount;
|
||||
} else {
|
||||
foreach ($socialProfiles as $name => $details) {
|
||||
if ($integrationObject = $integrationHelper->getIntegrationObject($name)) {
|
||||
if ($template = $integrationObject->getSocialProfileTemplate()) {
|
||||
$integrations[$name]['newContent'] = $this->renderView(
|
||||
$template,
|
||||
[
|
||||
'lead' => $lead,
|
||||
'details' => $details,
|
||||
'integrationName' => $name,
|
||||
'socialProfileUrls' => $socialProfileUrls,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$dataArray['profiles'] = $integrations;
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache for a network.
|
||||
*/
|
||||
public function clearSocialProfileAction(Request $request, IntegrationHelper $helper): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$network = InputHelper::clean($request->request->get('network'));
|
||||
$leadId = InputHelper::clean($request->request->get('lead'));
|
||||
|
||||
if (!empty($leadId)) {
|
||||
// find the lead
|
||||
$model = $this->getModel('lead.lead');
|
||||
$lead = $model->getEntity($leadId);
|
||||
|
||||
if (null !== $lead && $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editown', $lead->getPermissionUser())) {
|
||||
$dataArray['success'] = 1;
|
||||
$socialProfiles = $helper->clearIntegrationCache($lead, $network);
|
||||
$socialCount = count($socialProfiles);
|
||||
|
||||
if (empty($socialCount)) {
|
||||
$dataArray['completeProfile'] = $this->renderView(
|
||||
'@MauticLead/Social/index.html.twig',
|
||||
[
|
||||
'socialProfiles' => $socialProfiles,
|
||||
'lead' => $lead,
|
||||
'socialProfileUrls' => $helper->getSocialProfileUrlRegex(false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$dataArray['socialCount'] = $socialCount;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
protected function toggleLeadListAction(Request $request): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$listId = (int) $request->request->get('listId');
|
||||
$action = InputHelper::clean($request->request->get('listAction'));
|
||||
|
||||
if (!empty($leadId) && !empty($listId) && in_array($action, ['remove', 'add'])) {
|
||||
$leadModel = $this->getModel('lead');
|
||||
$listModel = $this->getModel('lead.list');
|
||||
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
$list = $listModel->getEntity($listId);
|
||||
|
||||
if (null !== $lead && null !== $list) {
|
||||
$class = 'add' == $action ? 'addToLists' : 'removeFromLists';
|
||||
$leadModel->$class($lead, $list);
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function togglePreferredLeadChannelAction(Request $request, LeadModel $leadModel, DoNotContactModel $doNotContact): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$channel = InputHelper::clean($request->request->get('channel'));
|
||||
$action = InputHelper::clean($request->request->get('channelAction'));
|
||||
|
||||
if (!empty($leadId) && !empty($channel) && in_array($action, ['remove', 'add'])) {
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
|
||||
if (null !== $lead) {
|
||||
if ('remove' === $action) {
|
||||
$doNotContact->addDncForContact($leadId, $channel, DoNotContact::MANUAL, 'user');
|
||||
} elseif ('add' === $action) {
|
||||
$doNotContact->removeDncForContact($leadId, $channel);
|
||||
$this->addFlashMessage('mautic.lead.event.donotcontact_channel_contactable', ['%channel%' => $channel], FlashBag::LEVEL_SUCCESS);
|
||||
$dataArray['flashes'] = $this->getFlashContent();
|
||||
}
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function toggleLeadCampaignAction(Request $request, MembershipManager $membershipManager, LeadModel $leadModel, CampaignModel $campaignModel): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$campaignId = (int) $request->request->get('campaignId');
|
||||
$action = InputHelper::clean($request->request->get('campaignAction'));
|
||||
|
||||
if (empty($leadId) || empty($campaignId) || !in_array($action, ['remove', 'add'])) {
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
$campaign = $campaignModel->getEntity($campaignId);
|
||||
|
||||
if (null === $lead || null === $campaign) {
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
if ('add' === $action) {
|
||||
$membershipManager->addContact($lead, $campaign);
|
||||
}
|
||||
|
||||
if ('remove' === $action) {
|
||||
$membershipManager->removeContact($lead, $campaign);
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function toggleCompanyLeadAction(Request $request, LeadModel $leadModel, CompanyModel $companyModel): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$companyId = (int) $request->request->get('companyId');
|
||||
$action = InputHelper::clean($request->request->get('companyAction'));
|
||||
|
||||
if (!empty($leadId) && !empty($companyId) && in_array($action, ['remove', 'add'])) {
|
||||
$lead = $leadModel->getEntity($leadId);
|
||||
$company = $companyModel->getEntity($companyId);
|
||||
|
||||
if (null !== $lead && null !== $company) {
|
||||
$class = 'add' == $action ? 'addLeadToCompany' : 'removeLeadFromCompany';
|
||||
$companyModel->$class($company, $lead);
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getImportProgressAction(Request $request): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 1];
|
||||
|
||||
if ($this->security->isGranted('lead:leads:create')) {
|
||||
$session = $request->getSession();
|
||||
$dataArray['progress'] = $session->get('mautic.lead.import.progress', [0, 0]);
|
||||
$dataArray['percent'] = ($dataArray['progress'][1]) ? ceil(($dataArray['progress'][0] / $dataArray['progress'][1]) * 100) : 100;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function removeBounceStatusAction(Request $request, DoNotContactModel $doNotContact, EmailModel $emailModel): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$dncId = $request->request->get('id');
|
||||
$channel = $request->request->get('channel', 'email');
|
||||
|
||||
if (!empty($dncId)) {
|
||||
/** @var DoNotContact $dnc */
|
||||
$dnc = $this->doctrine->getManager()->getRepository(DoNotContact::class)->findOneBy(
|
||||
[
|
||||
'id' => $dncId,
|
||||
]
|
||||
);
|
||||
|
||||
$lead = $dnc->getLead();
|
||||
if ($lead) {
|
||||
// Use lead model to trigger listeners
|
||||
$doNotContact->removeDncForContact($lead->getId(), $channel);
|
||||
$this->addFlashMessage('mautic.lead.event.donotcontact_channel_contactable', ['%channel%' => $channel], FlashBag::LEVEL_SUCCESS);
|
||||
$dataArray['flashes'] = $this->getFlashContent();
|
||||
} else {
|
||||
$emailModel->getRepository()->deleteDoNotEmailEntry($dncId);
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rows for new leads.
|
||||
*
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function getNewLeadsAction(Request $request, ContactColumnsDictionary $contactColumnsDictionary, LeadModel $model)
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$maxId = $request->get('maxId');
|
||||
|
||||
if (!empty($maxId)) {
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted(
|
||||
[
|
||||
'lead:leads:viewown',
|
||||
'lead:leads:viewother',
|
||||
'lead:leads:create',
|
||||
'lead:leads:editown',
|
||||
'lead:leads:editother',
|
||||
'lead:leads:deleteown',
|
||||
'lead:leads:deleteother',
|
||||
],
|
||||
'RETURN_ARRAY'
|
||||
);
|
||||
|
||||
if (!$permissions['lead:leads:viewown'] && !$permissions['lead:leads:viewother']) {
|
||||
return $this->accessDenied(true);
|
||||
}
|
||||
|
||||
$session = $request->getSession();
|
||||
$search = $session->get('mautic.lead.filter', '');
|
||||
$filter = ['string' => $search, 'force' => []];
|
||||
$translator = $this->translator;
|
||||
$anonymous = $translator->trans('mautic.lead.lead.searchcommand.isanonymous');
|
||||
$mine = $translator->trans('mautic.core.searchcommand.ismine');
|
||||
$indexMode = $session->get('mautic.lead.indexmode', 'list');
|
||||
|
||||
$session->set('mautic.lead.indexmode', $indexMode);
|
||||
|
||||
// (strpos($search, "$isCommand:$anonymous") === false && strpos($search, "$listCommand:") === false)) ||
|
||||
if ('list' != $indexMode) {
|
||||
// remove anonymous leads unless requested to prevent clutter
|
||||
$filter['force'][] = "!$anonymous";
|
||||
}
|
||||
|
||||
if (!$permissions['lead:leads:viewother']) {
|
||||
$filter['force'][] = $mine;
|
||||
}
|
||||
|
||||
$filter['force'][] = [
|
||||
'column' => 'l.id',
|
||||
'expr' => 'gt',
|
||||
'value' => $maxId,
|
||||
];
|
||||
|
||||
$results = $model->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
'withTotalCount' => true,
|
||||
]
|
||||
);
|
||||
$count = $results['count'];
|
||||
|
||||
if (!empty($count)) {
|
||||
// Get the max ID of the latest lead added
|
||||
$maxLeadId = $model->getRepository()->getMaxLeadId();
|
||||
|
||||
// We need the EmailRepository to check if a lead is flagged as do not contact
|
||||
/** @var \Mautic\EmailBundle\Entity\EmailRepository $emailRepo */
|
||||
$emailRepo = $this->getModel('email')->getRepository();
|
||||
$indexMode = $request->get('view', $session->get('mautic.lead.indexmode', 'list'));
|
||||
$template = ('list' == $indexMode) ? 'list_rows' : 'grid_cards';
|
||||
$dataArray['leads'] = $this->render(
|
||||
"@MauticLead/Lead/{$template}.html.twig",
|
||||
[
|
||||
'items' => $results['results'],
|
||||
'noContactList' => $emailRepo->getDoNotEmailList(array_keys($results['results'])),
|
||||
'permissions' => $permissions,
|
||||
'security' => $this->security,
|
||||
'highlight' => true,
|
||||
'currentList' => null,
|
||||
'columns' => $contactColumnsDictionary->getColumns(),
|
||||
]
|
||||
)->getContent();
|
||||
$dataArray['indexMode'] = $indexMode;
|
||||
$dataArray['maxId'] = $maxLeadId;
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getEmailTemplateAction(Request $request, EmailModel $model, MailHelper $mailHelper): JsonResponse
|
||||
{
|
||||
$data = ['success' => 1, 'body' => '', 'subject' => ''];
|
||||
$emailId = $request->query->get('template');
|
||||
|
||||
/** @var \Mautic\EmailBundle\Entity\Email $email */
|
||||
$email = $model->getEntity($emailId);
|
||||
|
||||
if (null !== $email
|
||||
&& $this->security->hasEntityAccess(
|
||||
'email:emails:viewown',
|
||||
'email:emails:viewother',
|
||||
$email->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
$mailHelper->setEmail($email, true, [], true);
|
||||
|
||||
$data['body'] = $mailHelper->getBody();
|
||||
$data['subject'] = $mailHelper->getSubject();
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function updateLeadTagsAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$post = $request->request->all()['lead_tags'] ?? [];
|
||||
$lead = $leadModel->getEntity((int) $post['id']);
|
||||
$updatedTags = (!empty($post['tags']) && is_array($post['tags'])) ? $post['tags'] : [];
|
||||
$data = ['success' => 0];
|
||||
|
||||
if (null !== $lead && $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())) {
|
||||
$leadModel->setTags($lead, $updatedTags, true);
|
||||
|
||||
/** @var \Doctrine\ORM\PersistentCollection $leadTags */
|
||||
$leadTags = $lead->getTags();
|
||||
$leadTagKeys = $leadTags->getKeys();
|
||||
|
||||
// Get an updated list of tags
|
||||
$tags = $leadModel->getTagRepository()->getSimpleList(null, [], 'tag');
|
||||
$tagOptions = '';
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$selected = (in_array($tag['label'], $leadTagKeys)) ? ' selected="selected"' : '';
|
||||
$tagOptions .= '<option'.$selected.' value="'.$tag['value'].'">'.$tag['label'].'</option>';
|
||||
}
|
||||
|
||||
$data['success'] = 1;
|
||||
$data['tags'] = $tagOptions;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function addLeadTagsAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$tags = $request->request->get('tags');
|
||||
$tags = json_decode($tags, true);
|
||||
|
||||
if (is_array($tags)) {
|
||||
$newTags = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (!is_numeric($tag)) {
|
||||
$newTags[] = $leadModel->getTagRepository()->getTagByNameOrCreateNewOne($tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($newTags)) {
|
||||
$leadModel->getTagRepository()->saveEntities($newTags);
|
||||
}
|
||||
|
||||
// Get an updated list of tags
|
||||
$allTags = $leadModel->getTagRepository()->getSimpleList(null, [], 'tag');
|
||||
$tagOptions = '';
|
||||
|
||||
foreach ($allTags as $tag) {
|
||||
$selected = (in_array($tag['value'], $tags) || in_array($tag['label'], $tags)) ? ' selected="selected"' : '';
|
||||
$tagOptions .= '<option'.$selected.' value="'.$tag['value'].'">'.$tag['label'].'</option>';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'tags' => $tagOptions,
|
||||
];
|
||||
} else {
|
||||
$data = ['success' => 0];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function addLeadUtmTagsAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$utmTags = $request->request->get('utmtags');
|
||||
$utmTags = json_decode($utmTags, true);
|
||||
|
||||
if (is_array($utmTags)) {
|
||||
$newUtmTags = [];
|
||||
foreach ($utmTags as $utmTag) {
|
||||
if (!is_numeric($utmTag)) {
|
||||
// New tag
|
||||
$utmTagEntity = new UtmTag();
|
||||
$utmTagEntity->setUtmTag(InputHelper::clean($utmTag));
|
||||
$newUtmTags[] = $utmTagEntity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($newUtmTags)) {
|
||||
$leadModel->getUtmTagRepository()->saveEntities($newUtmTags);
|
||||
}
|
||||
|
||||
// Get an updated list of tags
|
||||
$allUtmTags = $leadModel->getUtmTagRepository()->getSimpleList(null, [], 'utmtag');
|
||||
$utmTagOptions = '';
|
||||
|
||||
foreach ($allUtmTags as $utmTag) {
|
||||
$selected = (in_array($utmTag['value'], $utmTags) || in_array($utmTag['label'], $utmTags)) ? ' selected="selected"' : '';
|
||||
$utmTagOptions .= '<option'.$selected.' value="'.$utmTag['value'].'">'.$utmTag['label'].'</option>';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'tags' => $utmTagOptions,
|
||||
];
|
||||
} else {
|
||||
$data = ['success' => 0];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($data);
|
||||
}
|
||||
|
||||
public function reorderAction(Request $request, FieldModel $model): JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$order = InputHelper::clean($request->request->get('field'));
|
||||
$page = (int) $request->get('page');
|
||||
$limit = (int) $request->get('limit');
|
||||
|
||||
if (!empty($order)) {
|
||||
$startAt = ($page > 1) ? ($page * $limit) + 1 : 1;
|
||||
$model->reorderFieldsByList($order, $startAt);
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function updateLeadFieldValuesAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$alias = InputHelper::clean($request->request->get('alias'));
|
||||
$operator = InputHelper::clean($request->request->get('operator'));
|
||||
$changed = InputHelper::clean($request->request->get('changed'));
|
||||
$dataArray = ['success' => 0, 'options' => null, 'optionsAttr' => [], 'operators' => null, 'disabled' => false];
|
||||
$leadField = $this->getModel('lead.field')->getRepository()->findOneBy(['alias' => $alias]);
|
||||
|
||||
if ($leadField) {
|
||||
$options = null;
|
||||
$leadFieldType = $leadField->getType();
|
||||
|
||||
$properties = $leadField->getProperties();
|
||||
if (!empty($properties['list'])) {
|
||||
// Lookup/Select options
|
||||
$options = FormFieldHelper::parseList($properties['list']);
|
||||
} elseif (!empty($properties) && 'boolean' == $leadFieldType) {
|
||||
// Boolean options
|
||||
$options = [
|
||||
0 => $properties['no'],
|
||||
1 => $properties['yes'],
|
||||
];
|
||||
} else {
|
||||
switch ($leadFieldType) {
|
||||
case 'country':
|
||||
$options = FormFieldHelper::getCountryChoices();
|
||||
break;
|
||||
case 'region':
|
||||
$options = FormFieldHelper::getRegionChoices();
|
||||
break;
|
||||
case 'timezone':
|
||||
$options = FormFieldHelper::getTimezonesChoices();
|
||||
break;
|
||||
case 'locale':
|
||||
$options = array_flip(FormFieldHelper::getLocaleChoices());
|
||||
break;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
if ('date' == $operator) {
|
||||
$fieldHelper = new FormFieldHelper();
|
||||
$fieldHelper->setTranslator($this->translator);
|
||||
$options = $fieldHelper->getDateChoices();
|
||||
$options = array_merge(
|
||||
[
|
||||
'custom' => $this->translator->trans('mautic.campaign.event.timed.choice.custom'),
|
||||
],
|
||||
$options
|
||||
);
|
||||
|
||||
$dataArray['optionsAttr']['custom'] = [
|
||||
'data-custom' => 1,
|
||||
];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$options = (!empty($properties)) ? $properties : [];
|
||||
}
|
||||
}
|
||||
|
||||
$dataArray['fieldType'] = $leadFieldType;
|
||||
$dataArray['options'] = $options;
|
||||
|
||||
if ('field' === $changed) {
|
||||
$dataArray['operators'] = $leadModel->getOperatorsForFieldType($leadFieldType, ['date']);
|
||||
foreach ($dataArray['operators'] as $value => $label) {
|
||||
$dataArray['operators'][$value] = $this->translator->trans($label);
|
||||
}
|
||||
$operator = array_key_first($dataArray['operators']);
|
||||
}
|
||||
|
||||
$disabled = false;
|
||||
switch ($operator) {
|
||||
case 'empty':
|
||||
case '!empty':
|
||||
$disabled = true;
|
||||
$dataArray['options'] = null;
|
||||
break;
|
||||
case 'regexp':
|
||||
case '!regexp':
|
||||
$dataArray['options'] = null;
|
||||
break;
|
||||
}
|
||||
$dataArray['disabled'] = $disabled;
|
||||
}
|
||||
|
||||
$dataArray['success'] = 1;
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function setAsPrimaryCompanyAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$dataArray['success'] = 1;
|
||||
$companyId = InputHelper::clean($request->request->get('companyId'));
|
||||
$leadId = InputHelper::clean($request->request->get('leadId'));
|
||||
|
||||
$primaryCompany = $leadModel->setPrimaryCompany($companyId, $leadId);
|
||||
|
||||
$dataArray = array_merge($dataArray, $primaryCompany);
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getCampaignShareStatsAction(Request $request, SegmentCampaignShare $segmentCampaignShareService): JsonResponse
|
||||
{
|
||||
$ids = $request->query->all()['ids'] ?? [];
|
||||
$entityid = $request->query->get('entityId');
|
||||
|
||||
$data = $segmentCampaignShareService->getCampaignsSegmentShare((int) $entityid, $ids);
|
||||
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'stats' => $data,
|
||||
];
|
||||
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getLeadCountAction(Request $request, ListModel $model): JsonResponse
|
||||
{
|
||||
$id = (int) InputHelper::clean($request->get('id'));
|
||||
|
||||
$leadList = $model->getEntity($id);
|
||||
if (!$leadList) {
|
||||
return new JsonResponse($this->prepareJsonResponse(0, false), Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$leadCounts = $model->getSegmentContactCount([$id]);
|
||||
$leadCount = $leadCounts[$id];
|
||||
|
||||
return new JsonResponse(
|
||||
$this->prepareJsonResponse(
|
||||
$leadCount,
|
||||
$leadList->needsRebuild(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getSegmentDependencyTreeAction(Request $request, SegmentDependencyTreeFactory $segmentDependencyTreeFactory, ListModel $model): JsonResponse
|
||||
{
|
||||
$id = (int) $request->get('id');
|
||||
$segment = $model->getEntity($id);
|
||||
|
||||
if (!$segment) {
|
||||
return new JsonResponse(['message' => "Segment {$id} could not be found."], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$parentNode = $segmentDependencyTreeFactory->buildTree($segment);
|
||||
$formatter = new JsPlumbFormatter();
|
||||
|
||||
return new JsonResponse($formatter->format($parentNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function prepareJsonResponse(int $leadCount, bool $needsRebuild): array
|
||||
{
|
||||
return [
|
||||
'html' => $this->translator->trans(
|
||||
$needsRebuild ? 'mautic.lead.list.building' : 'mautic.lead.list.viewleads_count',
|
||||
['%count%' => $leadCount]
|
||||
),
|
||||
'className' => sprintf('label %s col-count', $needsRebuild ? 'label-info' : 'label-gray'),
|
||||
'leadCount' => $leadCount,
|
||||
];
|
||||
}
|
||||
|
||||
public function removeTagFromLeadAction(Request $request, LeadModel $leadModel): JsonResponse
|
||||
{
|
||||
$leadId = (int) $request->request->get('leadId');
|
||||
$tagId = (int) $request->request->get('tagId');
|
||||
|
||||
if (!empty($leadId) && !empty($tagId)) {
|
||||
$leadModel->removeTagFromLead($leadId, $tagId);
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse([]);
|
||||
}
|
||||
|
||||
public function updateLeadFieldOrderChoiceListAction(Request $request): Response
|
||||
{
|
||||
$object = InputHelper::clean($request->request->get('object'));
|
||||
$group = InputHelper::clean($request->request->get('group'));
|
||||
$field = new LeadField();
|
||||
$field->setObject($object);
|
||||
$field->setGroup($group);
|
||||
$form = $this->createForm(FieldType::class, $field);
|
||||
|
||||
return $this->render(
|
||||
'@MauticLead/Field/_field_order.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\LeadAccessTrait;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Company>
|
||||
*/
|
||||
class CompanyApiController extends CommonApiController
|
||||
{
|
||||
use CustomFieldsApiControllerTrait;
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* @var CompanyModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$companyModel = $modelFactory->getModel('lead.company');
|
||||
\assert($companyModel instanceof CompanyModel);
|
||||
|
||||
$this->model = $companyModel;
|
||||
$this->entityClass = Company::class;
|
||||
$this->entityNameOne = 'company';
|
||||
$this->entityNameMulti = 'companies';
|
||||
$this->serializerGroups[] = 'companyDetails';
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function getNewEntity(array $params)
|
||||
{
|
||||
$leadCompanyModel = $this->getModel('lead.company');
|
||||
\assert($leadCompanyModel instanceof CompanyModel);
|
||||
[$company, $companyEntities] = IdentifyCompanyHelper::findCompany($params, $leadCompanyModel);
|
||||
if (count($companyEntities)) {
|
||||
return $this->model->getEntity($company['id']);
|
||||
}
|
||||
|
||||
return $this->model->getEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
$this->setCustomFieldValues($entity, $form, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a contact to a company.
|
||||
*
|
||||
* @param int $companyId Company ID
|
||||
* @param int $contactId Contact ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function addContactAction($companyId, $contactId)
|
||||
{
|
||||
$company = $this->model->getEntity($companyId);
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
if (null === $company) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contact = $this->checkLeadAccess($contactId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
return $contact;
|
||||
}
|
||||
|
||||
$this->model->addLeadToCompany($company, $contact);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes given contact from a company.
|
||||
*
|
||||
* @param int $companyId List ID
|
||||
* @param int $contactId Lead ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function removeContactAction($companyId, $contactId)
|
||||
{
|
||||
$company = $this->model->getEntity($companyId);
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
if (null === $company) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contactModel = $this->getModel('lead');
|
||||
$contact = $contactModel->getEntity($contactId);
|
||||
|
||||
// Does the contact exist and the user has permission to edit
|
||||
if (null === $contact) {
|
||||
return $this->notFound();
|
||||
} elseif (!$this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $contact->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->model->removeLeadFromCompany($company, $contact);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CustomFieldEntityInterface;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
trait CustomFieldsApiControllerTrait
|
||||
{
|
||||
private ?RequestStack $requestStack = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private $fieldCache = [];
|
||||
|
||||
/**
|
||||
* Remove IpAddress and lastActive as it'll be handled outside the form.
|
||||
*
|
||||
* @param mixed[] $parameters
|
||||
* @param Lead|Company $entity
|
||||
* @param string $action
|
||||
*
|
||||
* @return mixed|void
|
||||
*/
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
if ('company' === $this->entityNameOne) {
|
||||
$object = 'company';
|
||||
} else {
|
||||
$object = 'lead';
|
||||
unset($parameters['lastActive'], $parameters['tags'], $parameters['ipAddress']);
|
||||
}
|
||||
|
||||
if (in_array($request->getMethod(), ['POST', 'PUT'])) {
|
||||
// If a new contact or PUT update (complete representation of the objectd), set empty fields to field defaults if the parameter
|
||||
// is not defined in the request
|
||||
|
||||
/** @var FieldModel $fieldModel */
|
||||
$fieldModel = $this->getModel('lead.field');
|
||||
$fields = $fieldModel->getFieldListWithProperties($object);
|
||||
|
||||
foreach ($fields as $alias => $field) {
|
||||
// Set the default value if the parameter is not included in the request, there is no value for the given entity, and a default is defined
|
||||
$currentValue = $entity->getFieldValue($alias);
|
||||
if (!isset($parameters[$alias]) && ('' === $currentValue || null == $currentValue) && '' !== $field['defaultValue'] && null !== $field['defaultValue']) {
|
||||
$parameters[$alias] = $field['defaultValue'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten fields into an 'all' key for dev convenience.
|
||||
*/
|
||||
protected function preSerializeEntity(object $entity, string $action = 'view'): void
|
||||
{
|
||||
if ($entity instanceof CustomFieldEntityInterface) {
|
||||
$fields = $entity->getFields();
|
||||
$fields['all'] = $entity->getProfileFields();
|
||||
|
||||
// Temporary hack to address numbers being type casted to float which broke some API implementations because M2 used to return
|
||||
// these as strings and values are normalized in a dozen differneet ways throughout LeadModel::setFieldValues methods and became
|
||||
// too risky to hotfix
|
||||
$fields = $this->fixNumbers($fields);
|
||||
|
||||
$entity->setFields($fields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $fields
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function fixNumbers(array $fields): array
|
||||
{
|
||||
$numberFields = [];
|
||||
foreach ($fields as $group => $groupFields) {
|
||||
if ('all' === $group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($groupFields as $field => $fieldDefinition) {
|
||||
if ('points' === $field) {
|
||||
// Points were always a number in M2
|
||||
$numberFields[$field] = (int) $fields[$group][$field]['value'];
|
||||
}
|
||||
|
||||
if ('number' !== $fieldDefinition['type'] || null === $fields[$group][$field]['value']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some requests don't seem to have properties unserialized by default (even in M2)
|
||||
if (!isset($fieldDefinition['properties'])) {
|
||||
$fieldDefinition['properties'] = [];
|
||||
}
|
||||
$properties = is_string($fieldDefinition['properties']) ? unserialize($fieldDefinition['properties']) : $fieldDefinition['properties'];
|
||||
|
||||
$fields[$group][$field]['value'] = empty($properties['scale']) ? (int) $fields[$group][$field]['value']
|
||||
: (float) $fields[$group][$field]['value'];
|
||||
$fields[$group][$field]['normalizedValue'] = empty($properties['scale']) ? (int) $fields[$group][$field]['normalizedValue']
|
||||
: (float) $fields[$group][$field]['normalizedValue'];
|
||||
|
||||
$numberFields[$field] = $fields[$group][$field]['value'];
|
||||
}
|
||||
}
|
||||
|
||||
// Fix "all" fields
|
||||
$fields['all'] = array_merge($fields['all'], $numberFields);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getEntityFormOptions(): array
|
||||
{
|
||||
$object = ('company' === $this->entityNameOne) ? 'company' : 'lead';
|
||||
|
||||
if (!empty($this->fieldCache[$object])) {
|
||||
return $this->fieldCache[$object];
|
||||
}
|
||||
|
||||
$model = $this->getModel('lead.field');
|
||||
\assert($model instanceof FieldModel);
|
||||
|
||||
$fields = $model->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'f.isPublished',
|
||||
'expr' => 'eq',
|
||||
'value' => true,
|
||||
],
|
||||
[
|
||||
'column' => 'f.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $object,
|
||||
],
|
||||
],
|
||||
],
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE),
|
||||
]
|
||||
);
|
||||
\assert($fields instanceof Paginator);
|
||||
|
||||
$this->fieldCache[$object] = ['fields' => $fields->getIterator()];
|
||||
|
||||
return $this->fieldCache[$object];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead|Company $entity
|
||||
* @param Form $form
|
||||
* @param mixed[] $parameters
|
||||
* @param bool $isPostOrPatch
|
||||
*
|
||||
* @return bool|void
|
||||
*/
|
||||
protected function setCustomFieldValues($entity, $form, $parameters, $isPostOrPatch = false)
|
||||
{
|
||||
// set the custom field values
|
||||
// pull the data from the form in order to apply the form's formatting
|
||||
foreach ($form as $f) {
|
||||
$parameters[$f->getName()] = $f->getData();
|
||||
}
|
||||
|
||||
if ($isPostOrPatch) {
|
||||
// Don't overwrite the contacts accumulated points
|
||||
if (isset($parameters['points']) && empty($parameters['points'])) {
|
||||
unset($parameters['points']);
|
||||
}
|
||||
|
||||
// When merging a contact because of a unique identifier match in POST /api/contacts//new or PATCH /api/contacts//edit all 0 values must be unset because
|
||||
// we have to assume 0 was not meant to overwrite an existing value. Other empty values will be caught by LeadModel::setFieldValues
|
||||
$parameters = array_filter(
|
||||
$parameters,
|
||||
function ($value): bool {
|
||||
if (is_numeric($value)) {
|
||||
return 0 !== (int) $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$overwriteWithBlank = !$isPostOrPatch;
|
||||
if (isset($parameters['overwriteWithBlank']) && !empty($parameters['overwriteWithBlank'])) {
|
||||
$overwriteWithBlank = true;
|
||||
unset($parameters['overwriteWithBlank']);
|
||||
}
|
||||
|
||||
$this->model->setFieldValues($entity, $parameters, $overwriteWithBlank);
|
||||
}
|
||||
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setRequestStack(RequestStack $requestStack): void
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\LeadAccessTrait;
|
||||
use Mautic\LeadBundle\Entity\LeadDevice;
|
||||
use Mautic\LeadBundle\Model\DeviceModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadDevice>
|
||||
*/
|
||||
class DeviceApiController extends CommonApiController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$leadDeviceModel = $modelFactory->getModel('lead.device');
|
||||
\assert($leadDeviceModel instanceof DeviceModel);
|
||||
|
||||
$this->model = $leadDeviceModel;
|
||||
$this->entityClass = LeadDevice::class;
|
||||
$this->entityNameOne = 'device';
|
||||
$this->entityNameMulti = 'devices';
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadDevice &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
$lead = null;
|
||||
if (!empty($parameters['lead'])) {
|
||||
$lead = $parameters['lead'];
|
||||
} elseif (!empty($parameters['contact'])) {
|
||||
$lead = $parameters['contact'];
|
||||
}
|
||||
if ($lead) {
|
||||
$lead = $this->checkLeadAccess($lead, $action);
|
||||
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$entity->setLead($lead);
|
||||
unset($parameters['lead'], $parameters['contact']);
|
||||
} elseif ('new' === $action) {
|
||||
return $this->returnError('contact ID is mandatory', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadDevice|FormEntity $entity
|
||||
*/
|
||||
protected function checkEntityAccess($entity, $action = 'view')
|
||||
{
|
||||
return parent::checkEntityAccess($entity->getLead(), $action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadField>
|
||||
*/
|
||||
class FieldApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* Can have value of 'contact' or 'company'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldObject;
|
||||
|
||||
/**
|
||||
* @var FieldModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper, FieldModel $fieldModel)
|
||||
{
|
||||
$request = $requestStack->getCurrentRequest();
|
||||
\assert(null !== $request);
|
||||
|
||||
$this->model = $fieldModel;
|
||||
$this->fieldObject = $request->get('object');
|
||||
$this->entityClass = LeadField::class;
|
||||
$this->entityNameOne = 'field';
|
||||
$this->entityNameMulti = 'fields';
|
||||
$this->routeParams = ['object' => $this->fieldObject];
|
||||
|
||||
if ('contact' === $this->fieldObject) {
|
||||
$this->fieldObject = 'lead';
|
||||
}
|
||||
|
||||
$repo = $this->model->getRepository();
|
||||
$tableAlias = $repo->getTableAlias();
|
||||
$this->listFilters[] = [
|
||||
'column' => $tableAlias.'.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $this->fieldObject,
|
||||
];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function saveEntity($entity, int $statusCode): int
|
||||
{
|
||||
try {
|
||||
return parent::saveEntity($entity, $statusCode);
|
||||
} catch (AbortColumnCreateException) {
|
||||
// Field has been queued
|
||||
return Response::HTTP_ACCEPTED;
|
||||
} catch (AbortColumnUpdateException) {
|
||||
// Field has been queued
|
||||
return Response::HTTP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and returns an array of where statements from the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getWhereFromRequest(Request $request)
|
||||
{
|
||||
$where = parent::getWhereFromRequest($request);
|
||||
|
||||
$where[] = [
|
||||
'col' => 'object',
|
||||
'expr' => 'eq',
|
||||
'val' => $this->fieldObject,
|
||||
];
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|void
|
||||
*/
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
$parameters['object'] = $this->fieldObject;
|
||||
// Workaround for mispelled isUniqueIdentifer.
|
||||
if (isset($parameters['isUniqueIdentifier'])) {
|
||||
$parameters['isUniqueIdentifer'] = $parameters['isUniqueIdentifier'];
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadField &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
if (isset($parameters['properties'])) {
|
||||
$result = $this->model->setFieldProperties($entity, $parameters['properties']);
|
||||
|
||||
if (true !== $result) {
|
||||
return $this->returnError($this->translator->trans($result, [], 'validators'), Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\ArrayHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\FrequencyRuleTrait;
|
||||
use Mautic\LeadBundle\Controller\LeadDetailsTrait;
|
||||
use Mautic\LeadBundle\DataObject\LeadManipulator;
|
||||
use Mautic\LeadBundle\Deduplicate\ContactMerger;
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Lead>
|
||||
*/
|
||||
class LeadApiController extends CommonApiController
|
||||
{
|
||||
use CustomFieldsApiControllerTrait;
|
||||
use FrequencyRuleTrait;
|
||||
use LeadDetailsTrait;
|
||||
|
||||
public const MODEL_ID = 'lead.lead';
|
||||
|
||||
/**
|
||||
* @var LeadModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
private DoNotContactModel $doNotContactModel;
|
||||
|
||||
public function __construct(
|
||||
CorePermissions $security,
|
||||
Translator $translator,
|
||||
EntityResultHelper $entityResultHelper,
|
||||
RouterInterface $router,
|
||||
FormFactoryInterface $formFactory,
|
||||
DoNotContactModel $doNotContactModel,
|
||||
AppVersion $appVersion,
|
||||
private ContactMerger $contactMerger,
|
||||
private UserHelper $userHelper,
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
RequestStack $requestStack,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
$this->doNotContactModel = $doNotContactModel;
|
||||
|
||||
$leadModel = $modelFactory->getModel(self::MODEL_ID);
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$this->model = $leadModel;
|
||||
$this->entityClass = Lead::class;
|
||||
$this->entityNameOne = 'contact';
|
||||
$this->entityNameMulti = 'contacts';
|
||||
$this->serializerGroups = ['leadDetails', 'frequencyRulesList', 'doNotContactList', 'userList', 'stageList', 'publishDetails', 'ipAddress', 'tagList', 'utmtagsList'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of users for lead owner edits.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getOwnersAction(Request $request)
|
||||
{
|
||||
if (!$this->security->isGranted(
|
||||
['lead:leads:create', 'lead:leads:editown', 'lead:leads:editother'],
|
||||
'MATCH_ONE'
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$filter = $request->query->get('filter', null);
|
||||
$limit = $request->query->get('limit', null);
|
||||
$start = $request->query->get('start', null);
|
||||
$users = $this->model->getLookupResults('user', $filter, $limit, $start);
|
||||
$view = $this->view($users, Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['userList']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
protected function getTotalCountTtl(): ?int
|
||||
{
|
||||
return $this->coreParametersHelper->get('contact_api_count_cache_ttl', 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of custom fields.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getFieldsAction()
|
||||
{
|
||||
if (!$this->security->isGranted(['lead:leads:editown', 'lead:leads:editother'], 'MATCH_ONE')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$fields = $this->getModel('lead.field')->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'f.isPublished',
|
||||
'expr' => 'eq',
|
||||
'value' => true,
|
||||
'object' => 'lead',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$view = $this->view($fields, Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['leadFieldList']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of notes on a specific lead.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getNotesAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$results = $this->getModel('lead.note')->getEntities(
|
||||
[
|
||||
'start' => $request->query->get('start', '0'),
|
||||
'limit' => $request->query->get('limit', $this->coreParametersHelper->get('default_pagelimit')),
|
||||
'filter' => [
|
||||
'string' => $request->query->get('search', ''),
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'n.lead',
|
||||
'expr' => 'eq',
|
||||
'value' => $entity,
|
||||
],
|
||||
],
|
||||
],
|
||||
'orderBy' => $request->query->get('orderBy', 'n.dateAdded'),
|
||||
'orderByDir' => $request->query->get('orderByDir', 'DESC'),
|
||||
]
|
||||
);
|
||||
|
||||
[$notes, $count] = $this->prepareEntitiesForView($results);
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => $count,
|
||||
'notes' => $notes,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
$context = $view->getContext()->setGroups(['leadNoteDetails']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of devices on a specific lead.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getDevicesAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$results = $this->getModel('lead.device')->getEntities(
|
||||
[
|
||||
'start' => $request->query->get('start', '0'),
|
||||
'limit' => $request->query->get('limit', $this->coreParametersHelper->get('default_pagelimit')),
|
||||
'filter' => [
|
||||
'string' => $request->query->get('search', ''),
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'd.lead',
|
||||
'expr' => 'eq',
|
||||
'value' => $entity,
|
||||
],
|
||||
],
|
||||
],
|
||||
'orderBy' => $request->query->get('orderBy', 'd.dateAdded'),
|
||||
'orderByDir' => $request->query->get('orderByDir', 'DESC'),
|
||||
]
|
||||
);
|
||||
|
||||
[$devices, $count] = $this->prepareEntitiesForView($results);
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => $count,
|
||||
'devices' => $devices,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
$context = $view->getContext()->setGroups(['leadDeviceDetails']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact segments the contact is in.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getListsAction($id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
if (null !== $entity) {
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lists = $this->model->getLists($entity, true, true);
|
||||
|
||||
foreach ($lists as &$l) {
|
||||
unset($l['leads'][0]['leadlist_id']);
|
||||
unset($l['leads'][0]['lead_id']);
|
||||
|
||||
$l = array_merge($l, $l['leads'][0]);
|
||||
|
||||
unset($l['leads']);
|
||||
}
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => count($lists),
|
||||
'lists' => $lists,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact companies the contact is in.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getCompaniesAction($id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$companies = $this->model->getCompanies($entity);
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => count($companies),
|
||||
'companies' => $companies,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of campaigns the lead is part of.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getCampaignsAction($id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
if (null !== $entity) {
|
||||
if (!$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother', $entity->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var \Mautic\CampaignBundle\Model\CampaignModel $campaignModel */
|
||||
$campaignModel = $this->getModel('campaign');
|
||||
$campaigns = $campaignModel->getLeadCampaigns($entity, true);
|
||||
|
||||
foreach ($campaigns as &$c) {
|
||||
if (!empty($c['lists'])) {
|
||||
$c['listMembership'] = array_keys($c['lists']);
|
||||
unset($c['lists']);
|
||||
}
|
||||
|
||||
unset($c['leads'][0]['campaign_id']);
|
||||
unset($c['leads'][0]['lead_id']);
|
||||
|
||||
$c = array_merge($c, $c['leads'][0]);
|
||||
|
||||
unset($c['leads']);
|
||||
}
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => count($campaigns),
|
||||
'campaigns' => $campaigns,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact events.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getActivityAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
return $this->getAllActivityAction($request, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of contact events.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getAllActivityAction(Request $request, $lead = null)
|
||||
{
|
||||
$canViewOwn = $this->security->isGranted('lead:leads:viewown');
|
||||
$canViewOthers = $this->security->isGranted('lead:leads:viewother');
|
||||
|
||||
if (!$canViewOthers && !$canViewOwn) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$filters = $this->sanitizeEventFilter(InputHelper::clean($request->query->all()['filters'] ?? $request->request->all()['filters'] ?? []));
|
||||
$limit = (int) $request->get('limit', 25);
|
||||
$page = (int) $request->get('page', 1);
|
||||
$order = InputHelper::clean($request->get('order', ['timestamp', 'DESC']));
|
||||
|
||||
[$events, $serializerGroups] = $this->model->getEngagements($lead, $filters, $order, $page, $limit, false);
|
||||
|
||||
$view = $this->view($events);
|
||||
$context = $view->getContext()->setGroups($serializerGroups);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a DNC to the contact.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function addDncAction(Request $request, $id, $channel)
|
||||
{
|
||||
$entity = $this->model->getEntity((int) $id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity, 'edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$channelId = (int) $request->request->get('channelId');
|
||||
if ($channelId) {
|
||||
$channel = [$channel => $channelId];
|
||||
}
|
||||
|
||||
// If no reason is set, default to 3 (manual)
|
||||
$reason = (int) $request->request->get('reason', DoNotContact::MANUAL);
|
||||
|
||||
// If a reason is set, but it's empty or 0, show an error.
|
||||
if (0 === $reason) {
|
||||
return $this->returnError(
|
||||
'Invalid reason code given',
|
||||
Response::HTTP_BAD_REQUEST,
|
||||
['Reason code needs to be an integer and higher than 0.']
|
||||
);
|
||||
}
|
||||
|
||||
$comments = InputHelper::clean($request->request->get('comments'));
|
||||
|
||||
$doNotContact = $this->doNotContactModel;
|
||||
$doNotContact->addDncForContact($entity->getId(), $channel, $reason, $comments);
|
||||
$view = $this->view([$this->entityNameOne => $entity]);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a DNC from the contact.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function removeDncAction($id, $channel)
|
||||
{
|
||||
$doNotContact = $this->doNotContactModel;
|
||||
|
||||
$entity = $this->model->getEntity((int) $id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity, 'edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$result = $doNotContact->removeDncForContact($entity->getId(), $channel);
|
||||
$view = $this->view(
|
||||
[
|
||||
'recordFound' => $result,
|
||||
$this->entityNameOne => $entity,
|
||||
]
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Remove a UTM Tagset to/from the contact.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $method
|
||||
* @param array<mixed>|int $data
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function applyUtmTagsAction($id, $method, $data)
|
||||
{
|
||||
$entity = $this->model->getEntity((int) $id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($entity, 'edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// calls add/remove method as appropriate
|
||||
$result = $this->model->$method($entity, $data);
|
||||
|
||||
if (false === $result) {
|
||||
return $this->badRequest();
|
||||
}
|
||||
|
||||
if ('removeUtmTags' == $method) {
|
||||
$view = $this->view(
|
||||
[
|
||||
'recordFound' => $result,
|
||||
$this->entityNameOne => $entity,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$view = $this->view([$this->entityNameOne => $entity]);
|
||||
}
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a UTM Tagset to the contact.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function addUtmTagsAction(Request $request, $id)
|
||||
{
|
||||
return $this->applyUtmTagsAction($id, 'addUTMTags', $request->request->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a UTM Tagset for the contact.
|
||||
*
|
||||
* @param int $id
|
||||
* @param int $utmid
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function removeUtmTagsAction($id, $utmid)
|
||||
{
|
||||
return $this->applyUtmTagsAction($id, 'removeUtmTags', (int) $utmid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new entity from provided params.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getNewEntity(array $params)
|
||||
{
|
||||
return $this->model->checkForDuplicateContact($params);
|
||||
}
|
||||
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
// Unset the tags from params to avoid a validation error
|
||||
if (isset($parameters['tags'])) {
|
||||
unset($parameters['tags']);
|
||||
}
|
||||
|
||||
// keep existing tags
|
||||
foreach ($entity->getTags() as $tag) {
|
||||
$parameters['tags'][] = $tag->getId();
|
||||
}
|
||||
|
||||
// keep existing owner if it is not set or should be reset to null
|
||||
if (!array_key_exists('owner', $parameters) && $entity->getOwner()) {
|
||||
$parameters['owner'] = $entity->getOwner()->getId();
|
||||
}
|
||||
// keep existing stage if it is not set or should be reset to null
|
||||
if (!array_key_exists('stage', $parameters) && $entity->getStage()) {
|
||||
$parameters['stage'] = $entity->getStage()->getId();
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $entity
|
||||
* @param array $parameters
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
if ('edit' === $action) {
|
||||
// Merge existing duplicate contact based on unique fields if exist
|
||||
// new endpoints will leverage getNewEntity in order to return the correct status codes
|
||||
$existingEntity = $this->model->checkForDuplicateContact($this->entityRequestParameters);
|
||||
\assert($existingEntity instanceof Lead);
|
||||
|
||||
$contactMerger = $this->contactMerger;
|
||||
|
||||
if ($entity->getId() && $existingEntity->getId()) {
|
||||
try {
|
||||
$entity = $contactMerger->merge($entity, $existingEntity);
|
||||
} catch (SameContactException) {
|
||||
}
|
||||
} elseif ($existingEntity->getId()) {
|
||||
$entity = $existingEntity;
|
||||
}
|
||||
}
|
||||
|
||||
$manipulatorObject = $this->inBatchMode ? 'api-batch' : 'api-single';
|
||||
|
||||
$entity->setManipulator(new LeadManipulator(
|
||||
'lead',
|
||||
$manipulatorObject,
|
||||
null,
|
||||
$this->userHelper->getUser()->getName()
|
||||
));
|
||||
|
||||
if (isset($parameters['companies'])) {
|
||||
$this->model->modifyCompanies($entity, $parameters['companies']);
|
||||
unset($parameters['companies']);
|
||||
}
|
||||
|
||||
if (isset($parameters['owner'])) {
|
||||
$owner = $this->getModel('user.user')->getEntity((int) $parameters['owner']);
|
||||
$entity->setOwner($owner);
|
||||
unset($parameters['owner']);
|
||||
}
|
||||
|
||||
if (isset($parameters['stage'])) {
|
||||
$stage = $this->getModel('stage.stage')->getEntity((int) $parameters['stage']);
|
||||
$entity->setStage($stage);
|
||||
unset($parameters['stage']);
|
||||
}
|
||||
|
||||
if (isset($this->entityRequestParameters['tags'])) {
|
||||
$this->model->modifyTags($entity, $this->entityRequestParameters['tags'], null, false);
|
||||
}
|
||||
|
||||
// Since the request can be from 3rd party, check for an IP address if included
|
||||
if (isset($this->entityRequestParameters['ipAddress'])) {
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress($this->entityRequestParameters['ipAddress']);
|
||||
\assert($ipAddress instanceof IpAddress);
|
||||
|
||||
if (!$entity->getIpAddresses()->contains($ipAddress)) {
|
||||
$entity->addIpAddress($ipAddress);
|
||||
}
|
||||
|
||||
unset($this->entityRequestParameters['ipAddress']);
|
||||
}
|
||||
|
||||
// Check for lastActive date
|
||||
if (isset($this->entityRequestParameters['lastActive'])) {
|
||||
$lastActive = new DateTimeHelper($this->entityRequestParameters['lastActive']);
|
||||
$entity->setLastActive($lastActive->getDateTime());
|
||||
unset($this->entityRequestParameters['lastActive']);
|
||||
}
|
||||
|
||||
// Batch DNC settings
|
||||
if (!empty($parameters['doNotContact']) && is_array($parameters['doNotContact'])) {
|
||||
foreach ($parameters['doNotContact'] as $dnc) {
|
||||
$channel = !empty($dnc['channel']) ? $dnc['channel'] : 'email';
|
||||
$comments = !empty($dnc['comments']) ? $dnc['comments'] : '';
|
||||
|
||||
$reason = (int) ArrayHelper::getValue('reason', $dnc, DoNotContact::MANUAL);
|
||||
|
||||
$doNotContact = $this->doNotContactModel;
|
||||
|
||||
if (DoNotContact::IS_CONTACTABLE === $reason) {
|
||||
if (!empty($entity->getId())) {
|
||||
// Remove DNC record
|
||||
$doNotContact->removeDncForContact($entity->getId(), $channel, false);
|
||||
}
|
||||
} elseif (empty($entity->getId())) {
|
||||
// Contact doesn't exist yet. Directly create a DNC record on the entity.
|
||||
$doNotContact->createDncRecord($entity, $channel, $reason, $comments);
|
||||
} else {
|
||||
// Add DNC record to existing contact
|
||||
$doNotContact->addDncForContact($entity->getId(), $channel, $reason, $comments, false);
|
||||
}
|
||||
}
|
||||
unset($parameters['doNotContact']);
|
||||
}
|
||||
|
||||
if (!empty($parameters['frequencyRules'])) {
|
||||
$viewParameters = [];
|
||||
$data = $this->getFrequencyRuleFormData($entity, null, null, false, $parameters['frequencyRules']);
|
||||
|
||||
if (true !== $frequencyForm = $this->getFrequencyRuleForm($entity, $viewParameters, $data)) {
|
||||
$formErrors = $this->getFormErrorMessages($frequencyForm);
|
||||
$msg = $this->getFormErrorMessage($formErrors);
|
||||
|
||||
if (!$msg) {
|
||||
$msg = $this->translator->trans('mautic.core.error.badrequest', [], 'flashes');
|
||||
}
|
||||
|
||||
return $this->returnError($msg, Response::HTTP_BAD_REQUEST, $formErrors);
|
||||
}
|
||||
|
||||
unset($parameters['frequencyRules']);
|
||||
}
|
||||
|
||||
$isPostOrPatch = 'POST' === $this->requestStack->getCurrentRequest()->getMethod() || 'PATCH' === $this->requestStack->getCurrentRequest()->getMethod();
|
||||
$this->setCustomFieldValues($entity, $form, $parameters, $isPostOrPatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to be used in FrequencyRuleTrait.
|
||||
*/
|
||||
protected function isFormCancelled(?FormInterface $form = null): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to be used in FrequencyRuleTrait.
|
||||
*/
|
||||
protected function isFormValid(FormInterface $form, ?array $data = null): bool
|
||||
{
|
||||
$form->submit($data, 'PATCH' !== $this->requestStack->getCurrentRequest()->getMethod());
|
||||
|
||||
return $form->isSubmitted() && $form->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $entity
|
||||
*/
|
||||
protected function detachEntity(object $entity): void
|
||||
{
|
||||
if (empty($entity->getPreviousId())) {
|
||||
parent::detachEntity($entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\LeadAccessTrait;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Security\Permissions\LeadPermissions;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadList>
|
||||
*/
|
||||
class ListApiController extends CommonApiController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* @var ListModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$listModel = $modelFactory->getModel('lead.list');
|
||||
\assert($listModel instanceof ListModel);
|
||||
|
||||
$this->model = $listModel;
|
||||
$this->entityClass = LeadList::class;
|
||||
$this->entityNameOne = 'list';
|
||||
$this->entityNameMulti = 'lists';
|
||||
$this->serializerGroups = [
|
||||
'leadListDetails',
|
||||
'userList',
|
||||
'publishDetails',
|
||||
'ipAddress',
|
||||
'categoryList',
|
||||
];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This conversion won't be needed in couple of years.
|
||||
*
|
||||
* The 'filter' and 'display' fields used to be part of each segment filter root array.
|
||||
* Those fields were moved to 'properties' subarray. We have to ensure BC and remove them
|
||||
* from filter root array so Symfony forms would not fail with unknown field error.
|
||||
*/
|
||||
protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action)
|
||||
{
|
||||
if (empty($parameters['filters']) || !is_array($parameters['filters'])) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
foreach ($parameters['filters'] as $key => $filter) {
|
||||
$bcFilterValue = $filter['filter'] ?? null;
|
||||
$filterValue = $filter['properties']['filter'] ?? $bcFilterValue;
|
||||
$parameters['filters'][$key]['properties']['filter'] = $filterValue;
|
||||
|
||||
if (!empty($filter['display']) && !isset($filter['properties']['display'])) {
|
||||
$parameters['filters'][$key]['properties']['display'] = $filter['display'];
|
||||
}
|
||||
|
||||
unset($parameters['filters'][$key]['filter'], $parameters['filters'][$key]['display']);
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of smart lists for the user.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getListsAction()
|
||||
{
|
||||
$listModel = $this->getModel('lead.list');
|
||||
\assert($listModel instanceof ListModel);
|
||||
$lists = $listModel->getUserLists();
|
||||
$view = $this->view($lists, Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['leadListList']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a lead to a list.
|
||||
*
|
||||
* @param int $id List ID
|
||||
* @param int $leadId Lead ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function addLeadAction($id, $leadId)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contact = $this->checkLeadAccess($leadId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
return $contact;
|
||||
}
|
||||
|
||||
// Does the user have access to the list
|
||||
$lists = $this->model->getUserLists();
|
||||
if (!isset($lists[$id])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$leadModel = $this->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$leadModel->addToLists($leadId, $entity);
|
||||
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a leads to a list.
|
||||
*
|
||||
* @param int $id segement ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function addLeadsAction(Request $request, $id)
|
||||
{
|
||||
$contactIds = $request->request->all()['ids'] ?? null;
|
||||
if (null === $contactIds) {
|
||||
return $this->returnError('mautic.core.error.badrequest', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
// Does the user have access to the list
|
||||
$lists = $this->model->getUserLists();
|
||||
if (!isset($lists[$id])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$responseDetail = [];
|
||||
foreach ($contactIds as $contactId) {
|
||||
$contact = $this->checkLeadAccess($contactId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
$responseDetail[$contactId] = ['success' => false];
|
||||
} else {
|
||||
$leadModel = $this->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
/* @var \Mautic\LeadBundle\Entity\Lead $contact */
|
||||
$leadModel->addToLists($contact, $entity);
|
||||
$responseDetail[$contact->getId()] = ['success' => true];
|
||||
}
|
||||
}
|
||||
|
||||
$view = $this->view(['success' => 1, 'details' => $responseDetail], Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes given contact from a list.
|
||||
*
|
||||
* @param int $id List ID
|
||||
* @param int $leadId Lead ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function removeLeadAction($id, $leadId)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contact = $this->checkLeadAccess($leadId, 'edit');
|
||||
if ($contact instanceof Response) {
|
||||
return $contact;
|
||||
}
|
||||
|
||||
// Does the user have access to the list
|
||||
$lists = $this->model->getUserLists();
|
||||
if (!isset($lists[$id])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$leadModel = $this->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$leadModel->removeFromLists($leadId, $entity);
|
||||
|
||||
$view = $this->view(['success' => 1], Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user has permission to access retrieved entity.
|
||||
*
|
||||
* @param mixed $entity
|
||||
* @param string $action view|create|edit|publish|delete
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkEntityAccess($entity, $action = 'view')
|
||||
{
|
||||
if ('create' == $action || 'edit' == $action || 'view' == $action) {
|
||||
return $this->security->isGranted(LeadPermissions::LISTS_VIEW_OWN);
|
||||
} elseif ('delete' == $action) {
|
||||
return $this->security->hasEntityAccess(
|
||||
true, LeadPermissions::LISTS_DELETE_OTHER, $entity->getCreatedBy()
|
||||
);
|
||||
}
|
||||
|
||||
return parent::checkEntityAccess($entity, $action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\LeadAccessTrait;
|
||||
use Mautic\LeadBundle\Entity\LeadNote;
|
||||
use Mautic\LeadBundle\Model\NoteModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<LeadNote>
|
||||
*/
|
||||
class NoteApiController extends CommonApiController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$leadNoteModel = $modelFactory->getModel('lead.note');
|
||||
\assert($leadNoteModel instanceof NoteModel);
|
||||
|
||||
$this->model = $leadNoteModel;
|
||||
$this->entityClass = LeadNote::class;
|
||||
$this->entityNameOne = 'note';
|
||||
$this->entityNameMulti = 'notes';
|
||||
$this->serializerGroups = ['leadNoteDetails', 'leadList'];
|
||||
|
||||
// When a user passes in a note like "This is <strong>text</strong>", this will
|
||||
// keep the HTML that was passed in.
|
||||
$this->dataInputMasks = ['text' => 'html'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Mautic\LeadBundle\Entity\Lead &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
if (!empty($parameters['lead'])) {
|
||||
$lead = $this->checkLeadAccess($parameters['lead'], $action);
|
||||
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$entity->setLead($lead);
|
||||
unset($parameters['lead']);
|
||||
} elseif ('new' === $action) {
|
||||
return $this->returnError('lead ID is mandatory', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\LeadBundle\Model\TagModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Tag>
|
||||
*/
|
||||
class TagApiController extends CommonApiController
|
||||
{
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$leadTagModel = $modelFactory->getModel('lead.tag');
|
||||
\assert($leadTagModel instanceof TagModel);
|
||||
|
||||
$this->model = $leadTagModel;
|
||||
$this->entityClass = Tag::class;
|
||||
$this->entityNameOne = 'tag';
|
||||
$this->entityNameMulti = 'tags';
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new entity from provided params.
|
||||
*
|
||||
* @return object
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getNewEntity(array $params)
|
||||
{
|
||||
if (empty($params[$this->entityNameOne])) {
|
||||
throw new \InvalidArgumentException($this->translator->trans('mautic.lead.api.tag.required', [], 'validators'));
|
||||
}
|
||||
|
||||
$tagModel = $this->model;
|
||||
\assert($tagModel instanceof TagModel);
|
||||
|
||||
return $tagModel->getRepository()->getTagByNameOrCreateNewOne($params[$this->entityNameOne]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Helper\ExportHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\DateHelper;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AuditlogController extends CommonController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
use LeadDetailsTrait;
|
||||
|
||||
public function indexAction(Request $request, $leadId, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.auditlog.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderbydir'),
|
||||
];
|
||||
|
||||
$events = $this->getAuditlogs($lead, $filters, $order, $page);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'events' => $events,
|
||||
'enableExportPermission' => $this->security->isAdmin() || $this->security->isGranted('report:export:enable', 'MATCH_ONE'),
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'leadAuditlog',
|
||||
'auditLogCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Auditlog/_list.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Response
|
||||
*/
|
||||
public function batchExportAction(Request $request, DateHelper $dateHelper, ExportHelper $exportHelper, $leadId)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted('report:export:enable', 'MATCH_ONE')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.auditlog.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.auditlog.orderbydir'),
|
||||
];
|
||||
|
||||
$dataType = $request->get('filetype', 'csv');
|
||||
|
||||
$resultsCallback = function ($event) use ($dateHelper): array {
|
||||
$eventLabel = $event['eventLabel'] ?? $event['eventType'];
|
||||
if (is_array($eventLabel)) {
|
||||
$eventLabel = $eventLabel['label'];
|
||||
}
|
||||
|
||||
return [
|
||||
'eventName' => $eventLabel,
|
||||
'eventType' => $event['eventType'] ?? '',
|
||||
'eventTimestamp' => $dateHelper->toText($event['timestamp'], 'local', 'Y-m-d H:i:s', true),
|
||||
];
|
||||
};
|
||||
|
||||
$results = $this->getAuditlogs($lead, $filters, $order, 1, 200);
|
||||
$count = $results['total'];
|
||||
$items = $results['events'];
|
||||
$iterations = ceil($count / 200);
|
||||
$loop = 1;
|
||||
|
||||
// Max of 50 iterations for 10K result export
|
||||
if ($iterations > 50) {
|
||||
$iterations = 50;
|
||||
}
|
||||
|
||||
$toExport = [];
|
||||
|
||||
while ($loop <= $iterations) {
|
||||
if (is_callable($resultsCallback)) {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = $resultsCallback($item);
|
||||
}
|
||||
} else {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = (array) $item;
|
||||
}
|
||||
}
|
||||
|
||||
$items = $this->getAuditlogs($lead, $filters, $order, $loop + 1, 200);
|
||||
|
||||
$this->doctrine->getManager()->clear();
|
||||
|
||||
++$loop;
|
||||
}
|
||||
|
||||
return $this->exportResultsAs($toExport, $dataType, 'contact_auditlog', $exportHelper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\AbstractFormController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Form\Type\BatchType;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\LeadBundle\Model\SegmentActionModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class BatchSegmentController extends AbstractFormController
|
||||
{
|
||||
public function __construct(
|
||||
private SegmentActionModel $segmentActionModel,
|
||||
private ListModel $segmentModel,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* API for batch action.
|
||||
*/
|
||||
public function setAction(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->query->all()['lead_batch'] ?? $request->request->all()['lead_batch'] ?? [];
|
||||
$contactIds = empty($params['ids']) ? [] : json_decode($params['ids']);
|
||||
|
||||
if ($contactIds && is_array($contactIds)) {
|
||||
$segmentsToAdd = $params['add'] ?? [];
|
||||
$segmentsToRemove = $params['remove'] ?? [];
|
||||
|
||||
if ($segmentsToAdd) {
|
||||
$this->segmentActionModel->addContacts($contactIds, $segmentsToAdd);
|
||||
}
|
||||
|
||||
if ($segmentsToRemove) {
|
||||
$this->segmentActionModel->removeContacts($contactIds, $segmentsToRemove);
|
||||
}
|
||||
|
||||
$this->addFlashMessage('mautic.lead.batch_leads_affected', [
|
||||
'%count%' => count($contactIds),
|
||||
]);
|
||||
} else {
|
||||
$this->addFlashMessage('mautic.core.error.ids.missing');
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'closeModal' => true,
|
||||
'flashes' => $this->getFlashContent(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View for batch action.
|
||||
*/
|
||||
public function indexAction(): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$route = $this->generateUrl('mautic_segment_batch_contact_set');
|
||||
$lists = $this->segmentModel->getUserLists();
|
||||
$items = [];
|
||||
|
||||
foreach ($lists as $list) {
|
||||
$items[$list['name'].' ('.$list['id'].')'] = $list['id'];
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $this->createForm(
|
||||
BatchType::class,
|
||||
[],
|
||||
[
|
||||
'items' => $items,
|
||||
'action' => $route,
|
||||
]
|
||||
)->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Batch/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contact_index',
|
||||
'mauticContent' => 'leadBatch',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
trait EntityContactsTrait
|
||||
{
|
||||
/**
|
||||
* @param string|int $entityId
|
||||
* @param int $page
|
||||
* @param string[]|string $permission
|
||||
* @param string $sessionVar
|
||||
* @param string $entityJoinTable Table to join to obtain list of related contacts or a DBAL QueryBuilder object defining custom joins
|
||||
* @param string|null $dncChannel Channel for this entity to get do not contact records for
|
||||
* @param string|null $entityIdColumnName If the entity ID in $joinTable is not "id", set the column name here
|
||||
* @param array|null $contactFilter Array of additional filters for the getEntityContactsWithFields() function
|
||||
* @param array|null $additionalJoins [ ['type' => 'join|leftJoin', 'from_alias' => '', 'table' => '', 'condition' => ''], ... ]
|
||||
* @param string|null $contactColumnName Column of the contact in the join table
|
||||
* @param string|null $paginationTarget DOM selector for injecting new content when pagination is used
|
||||
* @param string|null $orderBy optional OrderBy column, to be used to increase performance with joins
|
||||
* @param string|null $orderByDir optional $orderBy direction, to be used to increase performance with joins
|
||||
* @param int|null $count optional $count if already known to avoid an extra query
|
||||
* @param \DateTimeInterface|null $dateFrom optionally limit to leads added between From and To dates
|
||||
* @param \DateTimeInterface|null $dateTo optionally limit to leads added between From and To dates
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function generateContactsGrid(
|
||||
Request $request,
|
||||
PageHelperFactoryInterface $pageHelperFactory,
|
||||
$entityId,
|
||||
$page,
|
||||
$permission,
|
||||
$sessionVar,
|
||||
$entityJoinTable,
|
||||
$dncChannel = null,
|
||||
$entityIdColumnName = 'id',
|
||||
?array $contactFilter = null,
|
||||
?array $additionalJoins = null,
|
||||
$contactColumnName = null,
|
||||
?array $routeParameters = [],
|
||||
$paginationTarget = null,
|
||||
$orderBy = null,
|
||||
$orderByDir = null,
|
||||
$count = null,
|
||||
?\DateTimeInterface $dateFrom = null,
|
||||
?\DateTimeInterface $dateTo = null,
|
||||
) {
|
||||
if ($permission && !$this->security->isGranted($permission)) {
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'page' => $page,
|
||||
'items' => [], // return 0 contacts if user has no permissions
|
||||
'totalItems' => 0,
|
||||
'tmpl' => $sessionVar.'Contacts',
|
||||
'indexMode' => 'grid',
|
||||
'routeParameters' => $routeParameters,
|
||||
'sessionVar' => $sessionVar.'.contact',
|
||||
'objectId' => $entityId,
|
||||
'target' => $paginationTarget,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Lead/grid.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => $sessionVar.'Contacts',
|
||||
'route' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Set the route if not standardized
|
||||
$route = "mautic_{$sessionVar}_contacts";
|
||||
if (method_exists($this, 'getRouteBase') && $this->getRouteBase()) {
|
||||
$route = 'mautic_'.$this->getRouteBase().'_contacts';
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if ('POST' == $request->getMethod()) {
|
||||
$this->setListFilters($sessionVar.'.contact');
|
||||
}
|
||||
|
||||
$search = $request->get('search', $request->getSession()->get('mautic.'.$sessionVar.'.contact.filter', ''));
|
||||
$request->getSession()->set('mautic.'.$sessionVar.'.contact.filter', $search);
|
||||
|
||||
$pageHelper = $pageHelperFactory->make("mautic.{$sessionVar}", $page);
|
||||
|
||||
$filter = ['string' => $search, 'force' => []];
|
||||
$orderBy = $orderBy ?: $request->getSession()->get('mautic.'.$sessionVar.'.contact.orderby', 'l.id');
|
||||
$orderByDir = $orderByDir ?: $request->getSession()->get('mautic.'.$sessionVar.'.contact.orderbydir', 'DESC');
|
||||
|
||||
$limit = $request->getSession()->get(
|
||||
'mautic.'.$sessionVar.'.contact.limit',
|
||||
$this->coreParametersHelper->get('default_pagelimit')
|
||||
);
|
||||
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
/** @var LeadRepository $repo */
|
||||
$repo = $this->getModel('lead')->getRepository();
|
||||
$contacts = $repo->getEntityContacts(
|
||||
[
|
||||
'withTotalCount' => (null === $count),
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
'select' => ListController::SEGMENT_CONTACT_FIELDS,
|
||||
'route' => $route,
|
||||
],
|
||||
$entityJoinTable,
|
||||
$entityId,
|
||||
$contactFilter,
|
||||
$entityIdColumnName,
|
||||
$additionalJoins,
|
||||
$contactColumnName,
|
||||
$dateFrom,
|
||||
$dateTo
|
||||
);
|
||||
|
||||
// Normalize results regarding withTotalCount.
|
||||
if (isset($contacts['count'])) {
|
||||
$count = (int) $contacts['count'];
|
||||
} else {
|
||||
$contacts = [
|
||||
'results' => $contacts,
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
|
||||
if ($count && $count < ($start + 1)) {
|
||||
// the number of entities are now less then the current page so redirect to the last page
|
||||
$lastPage = $pageHelper->countPage($count);
|
||||
$pageHelper->rememberPage($lastPage);
|
||||
$returnUrl = $this->generateUrl($route, array_merge(['objectId' => $entityId, 'page' => $lastPage], $routeParameters));
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $lastPage, 'objectId' => $entityId],
|
||||
'contentTemplate' => '@MauticLead/Lead/grid.html.twig',
|
||||
'forwardController' => false,
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => $sessionVar.'Contacts',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$pageHelper->rememberPage($page);
|
||||
|
||||
// Get DNC for the contact
|
||||
$dnc = [];
|
||||
if ($dncChannel && $count > 0) {
|
||||
$dnc = $this->doctrine->getManager()->getRepository(\Mautic\LeadBundle\Entity\DoNotContact::class)->getChannelList(
|
||||
$dncChannel,
|
||||
array_keys($contacts['results'])
|
||||
);
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'page' => $page,
|
||||
'items' => $contacts['results'],
|
||||
'totalItems' => $contacts['count'],
|
||||
'tmpl' => $sessionVar.'Contacts',
|
||||
'indexMode' => 'grid',
|
||||
'route' => $route,
|
||||
'routeParameters' => $routeParameters,
|
||||
'sessionVar' => $sessionVar.'.contact',
|
||||
'limit' => $limit,
|
||||
'objectId' => $entityId,
|
||||
'noContactList' => $dnc,
|
||||
'target' => $paginationTarget,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Lead/grid.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => $sessionVar.'Contacts',
|
||||
'route' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Exception\SchemaException;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Helper\FieldAliasHelper;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class FieldController extends FormController
|
||||
{
|
||||
/**
|
||||
* Generate's default list view.
|
||||
*
|
||||
* @param int $page
|
||||
*
|
||||
* @return array|JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, FieldModel $fieldModel, $page = 1)
|
||||
{
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted(['lead:fields:view', 'lead:fields:full'], 'RETURN_ARRAY');
|
||||
|
||||
$session = $request->getSession();
|
||||
|
||||
if (!$permissions['lead:fields:view'] && !$permissions['lead:fields:full']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$limit = $session->get('mautic.leadfield.limit', $this->coreParametersHelper->get('default_pagelimit'));
|
||||
$search = $request->get('search', $session->get('mautic.leadfield.filter', ''));
|
||||
$session->set('mautic.leadfield.filter', $search);
|
||||
|
||||
// do some default filtering
|
||||
$orderBy = $request->getSession()->get('mautic.leadfield.orderby', 'f.order');
|
||||
$orderByDir = $request->getSession()->get('mautic.leadfield.orderbydir', 'ASC');
|
||||
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
$fields = $fieldModel->getEntities([
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => [
|
||||
'string' => $search,
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'f.columnIsNotRemoved',
|
||||
'value' => false,
|
||||
'expr' => 'eq',
|
||||
],
|
||||
],
|
||||
],
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]);
|
||||
$count = count($fields);
|
||||
|
||||
if ($count && $count < ($start + 1)) {
|
||||
// the number of entities are now less then the current page so redirect to the last page
|
||||
if (1 === $count) {
|
||||
$lastPage = 1;
|
||||
} else {
|
||||
$lastPage = (ceil($count / $limit)) ?: 1;
|
||||
}
|
||||
$session->set('mautic.leadfield.page', $lastPage);
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index', ['page' => $lastPage]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $lastPage],
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// set what page currently on so that we can return here after form submission/cancellation
|
||||
$session->set('mautic.leadfield.page', $page);
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'items' => $fields,
|
||||
'searchValue' => $search,
|
||||
'permissions' => $permissions,
|
||||
'tmpl' => $tmpl,
|
||||
'totalItems' => $count,
|
||||
'limit' => $limit,
|
||||
'page' => $page,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $this->generateUrl('mautic_contactfield_index', ['page' => $page]),
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's new form and processes post data.
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, ?LeadField $entity = null)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// retrieve the entity
|
||||
$field = $entity instanceof LeadField ? $entity : new LeadField();
|
||||
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
// set the return URL for post actions
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'new']);
|
||||
// get the user form factory
|
||||
$form = $model->createForm($field, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$requestData = $request->request->all();
|
||||
if (isset($requestData['leadfield']['properties'])) {
|
||||
$result = $model->setFieldProperties($field, $requestData['leadfield']['properties']);
|
||||
if (true !== $result) {
|
||||
// set the error
|
||||
$form->get('properties')->addError(
|
||||
new FormError(
|
||||
$this->translator->trans($result, [], 'validators')
|
||||
)
|
||||
);
|
||||
$valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
$flashMessage = 'mautic.core.notice.created';
|
||||
try {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($field);
|
||||
} catch (\Doctrine\DBAL\Exception $ee) {
|
||||
$flashMessage = $ee->getMessage();
|
||||
} catch (AbortColumnCreateException) {
|
||||
$flashMessage = $this->translator->trans('mautic.lead.field.pushed_to_background');
|
||||
} catch (SchemaException $e) {
|
||||
$flashMessage = $e->getMessage();
|
||||
$form['alias']->addError(new FormError($e->getMessage()));
|
||||
$valid = false;
|
||||
} catch (\Exception $e) {
|
||||
$form['alias']->addError(
|
||||
new FormError(
|
||||
$this->translator->trans('mautic.lead.field.failed', ['%error%' => $e->getMessage()], 'validators')
|
||||
)
|
||||
);
|
||||
$valid = false;
|
||||
}
|
||||
$this->addFlashMessage(
|
||||
$flashMessage,
|
||||
[
|
||||
'%name%' => $field->getLabel(),
|
||||
'%menu_link%' => 'mautic_contactfield_index',
|
||||
'%url%' => $this->generateUrl(
|
||||
'mautic_contactfield_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $field->getId(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]
|
||||
);
|
||||
} elseif ($valid && !$cancelled) {
|
||||
return $this->editAction($request, $field->getId(), true);
|
||||
} elseif (!$valid) {
|
||||
// some bug in Symfony prevents repopulating list options on errors
|
||||
$field = $form->getData();
|
||||
$newForm = $model->createForm($field, $this->formFactory, $action);
|
||||
$this->copyErrorsRecursively($form, $newForm);
|
||||
$form = $newForm;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'leadField' => $entity,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'new']),
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's edit form and processes post data.
|
||||
*
|
||||
* @param bool|false $ignorePost
|
||||
*
|
||||
* @return array|JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
$field = $model->getEntity($objectId);
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
];
|
||||
// list not found
|
||||
if (null === $field) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.field.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
} elseif ($model->isLocked($field)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $field, 'lead.field');
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$form = $model->createForm($field, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$requestData = $request->request->all();
|
||||
if (isset($requestData['leadfield']['properties'])) {
|
||||
$result = $model->setFieldProperties($field, $requestData['leadfield']['properties']);
|
||||
if (true !== $result) {
|
||||
// set the error
|
||||
$form->get('properties')->addError(new FormError(
|
||||
$this->translator->trans($result, [], 'validators')
|
||||
));
|
||||
$valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
$flashMessage = 'mautic.core.notice.updated';
|
||||
|
||||
// form is valid so process the data
|
||||
try {
|
||||
$model->saveEntity($field, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
} catch (AbortColumnUpdateException) {
|
||||
$flashMessage = $this->translator->trans('mautic.lead.field.update_pushed_to_background');
|
||||
} catch (SchemaException $e) {
|
||||
$flashMessage = $e->getMessage();
|
||||
$form['alias']->addError(new FormError($e->getMessage()));
|
||||
$valid = false;
|
||||
}
|
||||
|
||||
$this->addFlashMessage($flashMessage, [
|
||||
'%name%' => $field->getLabel(),
|
||||
'%menu_link%' => 'mautic_contactfield_index',
|
||||
'%url%' => $this->generateUrl('mautic_contactfield_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $field->getId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// unlock the entity
|
||||
$model->unlockEntity($field);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'viewParameters' => ['objectId' => $field->getId()],
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
]
|
||||
)
|
||||
);
|
||||
} elseif ($valid) {
|
||||
// Rebuild the form with new action so that apply doesn't keep creating a clone
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'edit', 'objectId' => $field->getId()]);
|
||||
$form = $model->createForm($field, $this->formFactory, $action);
|
||||
} else {
|
||||
// some bug in Symfony prevents repopulating list options on errors
|
||||
$field = $form->getData();
|
||||
$newForm = $model->createForm($field, $this->formFactory, $action);
|
||||
$this->copyErrorsRecursively($form, $newForm);
|
||||
$form = $newForm;
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($field);
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $action,
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*/
|
||||
public function cloneAction(Request $request, FieldAliasHelper $fieldAliasHelper, FieldModel $fieldModel, $objectId): RedirectResponse|Response
|
||||
{
|
||||
$entity = $fieldModel->getEntity($objectId);
|
||||
|
||||
if (!$entity) {
|
||||
throw $this->createNotFoundException('Entity not found');
|
||||
}
|
||||
|
||||
$clone = clone $entity;
|
||||
|
||||
$fieldAliasHelper->makeAliasUnique($clone);
|
||||
|
||||
$action = $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'new']);
|
||||
$form = $fieldModel->createForm($clone, $this->formFactory, $action);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'leadField' => $clone,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Field/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'route' => $this->generateUrl('mautic_contactfield_action', ['objectAction' => 'clone', 'objectId' => $objectId]),
|
||||
'mauticContent' => 'leadfield',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a field.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'lead',
|
||||
],
|
||||
];
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
$field = $model->getEntity($objectId);
|
||||
|
||||
if (null === $field) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.field.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif ($model->isLocked($field)) {
|
||||
return $this->isLocked($postActionVars, $field, 'lead.field');
|
||||
} elseif ($field->isFixed()) {
|
||||
// cannot delete fixed fields
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$segments = [];
|
||||
foreach ($model->getFieldSegments($field) as $segment) {
|
||||
$segments[] = sprintf('"%s" (%d)', $segment->getName(), $segment->getId());
|
||||
}
|
||||
|
||||
if (count($segments)) {
|
||||
$flashMessage = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.core.notice.used.field',
|
||||
'msgVars' => [
|
||||
'%name%' => $field->getLabel(),
|
||||
'%id%' => $objectId,
|
||||
'%segments%' => implode(', ', $segments),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$model->deleteEntity($field);
|
||||
$flashMessage = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $field->getLabel(),
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$flashes[] = $flashMessage;
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function batchDeleteAction(Request $request)
|
||||
{
|
||||
if (!$this->security->isGranted('lead:fields:full')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_contactfield_index');
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\LeadBundle\Controller\FieldController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_contactfield_index',
|
||||
'mauticContent' => 'lead',
|
||||
],
|
||||
];
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
/** @var FieldModel $model */
|
||||
$model = $this->getModel('lead.field');
|
||||
$ids = json_decode($request->query->get('ids', '{}'));
|
||||
$deleteIds = [];
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.field.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif ($entity->isFixed()) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'lead.field', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$filteredDeleteIds = $model->filterUsedFieldIds($deleteIds);
|
||||
$usedFieldIds = array_diff($deleteIds, $filteredDeleteIds);
|
||||
$segments = [];
|
||||
$usedFieldsNames = [];
|
||||
|
||||
// Iterating through all used fileds to get segments they are used in
|
||||
foreach ($usedFieldIds as $usedFieldId) {
|
||||
$fieldEntity = $model->getEntity($usedFieldId);
|
||||
foreach ($model->getFieldSegments($fieldEntity) as $segment) {
|
||||
$segments[$segment->getId()] = sprintf('"%s" (%d)', $segment->getName(), $segment->getId());
|
||||
$usedFieldsNames[] = sprintf('"%s"', $fieldEntity->getName());
|
||||
}
|
||||
}
|
||||
|
||||
if ($filteredDeleteIds !== $deleteIds) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.core.notice.used.fields',
|
||||
'msgVars' => [
|
||||
'%segments%' => implode(', ', $segments),
|
||||
'%fields%' => implode(', ', array_unique($usedFieldsNames)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (count($filteredDeleteIds)) {
|
||||
$entities = $model->deleteEntities($filteredDeleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.lead.field.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Form\Type\ContactFrequencyType;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
trait FrequencyRuleTrait
|
||||
{
|
||||
protected $leadLists;
|
||||
|
||||
protected $dncChannels;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $isPublicView = false;
|
||||
|
||||
private \Mautic\LeadBundle\Model\DoNotContact $doNotContactModel;
|
||||
|
||||
private ?RequestStack $requestStack = null;
|
||||
|
||||
/**
|
||||
* @param array $viewParameters
|
||||
* @param bool $isPublic
|
||||
* @param bool $isPreferenceCenter
|
||||
*
|
||||
* @return true|FormInterface
|
||||
*/
|
||||
protected function getFrequencyRuleForm($lead, &$viewParameters = [], &$data = null, $isPublic = false, $action = null, $isPreferenceCenter = false)
|
||||
{
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
|
||||
$leadChannels = $model->getContactChannels($lead);
|
||||
$allChannels = $model->getPreferenceChannels();
|
||||
$leadLists = $model->getLists($lead, true, true, $isPublic, $isPreferenceCenter);
|
||||
|
||||
$viewParameters = array_merge(
|
||||
$viewParameters,
|
||||
[
|
||||
'leadsLists' => $leadLists,
|
||||
'channels' => $allChannels,
|
||||
'leadChannels' => $leadChannels,
|
||||
]
|
||||
);
|
||||
|
||||
// find the email
|
||||
$currentChannelId = null;
|
||||
if (!empty($viewParameters['idHash'])) {
|
||||
$emailModel = $this->getModel('email');
|
||||
\assert($emailModel instanceof EmailModel);
|
||||
if ($stat = $emailModel->getEmailStatus($viewParameters['idHash'])) {
|
||||
if ($email = $stat->getEmail()) {
|
||||
$currentChannelId = $email->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null == $data) {
|
||||
$data = $this->getFrequencyRuleFormData($lead, $allChannels, $leadChannels, $isPublic, null, $isPreferenceCenter);
|
||||
}
|
||||
/** @var FormInterface $form */
|
||||
$form = $this->formFactory->create(
|
||||
ContactFrequencyType::class,
|
||||
$data,
|
||||
[
|
||||
'action' => $action,
|
||||
'channels' => $allChannels,
|
||||
'public_view' => $isPublic,
|
||||
'preference_center_only' => $isPreferenceCenter,
|
||||
'allow_extra_fields' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
\assert(null !== $request);
|
||||
$method = $request->getMethod();
|
||||
if ('GET' !== $method) {
|
||||
if (!$this->isFormCancelled($form)) {
|
||||
if ($this->isFormValid($form)) {
|
||||
$this->persistFrequencyRuleFormData($lead, $form->getData(), $allChannels, $leadChannels, $currentChannelId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isPublic
|
||||
*/
|
||||
protected function getFrequencyRuleFormData(Lead $lead, ?array $allChannels = null, $leadChannels = null, $isPublic = false, $frequencyRules = null, $isPreferenceCenter = false): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
if (null === $allChannels) {
|
||||
$allChannels = $model->getPreferenceChannels();
|
||||
}
|
||||
|
||||
if (null === $leadChannels) {
|
||||
$leadChannels = $model->getContactChannels($lead);
|
||||
}
|
||||
|
||||
if (null === $frequencyRules) {
|
||||
$frequencyRules = $model->getFrequencyRules($lead);
|
||||
}
|
||||
|
||||
foreach ($allChannels as $channel) {
|
||||
if (isset($frequencyRules[$channel])) {
|
||||
$frequencyRule = $frequencyRules[$channel];
|
||||
$data['lead_channels']['frequency_number_'.$channel] = $frequencyRule['frequency_number'];
|
||||
$data['lead_channels']['frequency_time_'.$channel] = $frequencyRule['frequency_time'];
|
||||
if ($frequencyRule['pause_from_date']) {
|
||||
$data['lead_channels']['contact_pause_start_date_'.$channel] = new \DateTime($frequencyRule['pause_from_date']);
|
||||
}
|
||||
|
||||
if ($frequencyRule['pause_to_date']) {
|
||||
$data['lead_channels']['contact_pause_end_date_'.$channel] = new \DateTime($frequencyRule['pause_to_date']);
|
||||
}
|
||||
|
||||
if (!empty($frequencyRule['preferred_channel'])) {
|
||||
$data['lead_channels']['preferred_channel'] = $channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data['global_categories'] = $frequencyRules['global_categories'] ?? $model->getSubscribedAndNewCategoryIds(
|
||||
$lead, ['global', 'email']);
|
||||
|
||||
$this->leadLists = $model->getLists($lead, false, false, $isPublic, $isPreferenceCenter);
|
||||
$data['lead_lists'] = [];
|
||||
foreach ($this->leadLists as $leadList) {
|
||||
$data['lead_lists'][] = $leadList->getId();
|
||||
}
|
||||
|
||||
$data['lead_channels']['subscribed_channels'] = $leadChannels;
|
||||
$this->isPublicView = $isPublic;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $currentChannelId
|
||||
*/
|
||||
protected function persistFrequencyRuleFormData(Lead $lead, array $formData, array $allChannels, $leadChannels, $currentChannelId = null)
|
||||
{
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = $this->getModel('lead.lead');
|
||||
|
||||
$dncModel = $this->doNotContactModel;
|
||||
\assert($dncModel instanceof \Mautic\LeadBundle\Model\DoNotContact);
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
\assert(null !== $request);
|
||||
// iF subscribed_channels are enabled in form, then touch DNC
|
||||
if (isset($request->request->all()['lead_contact_frequency_rules']['lead_channels'])) {
|
||||
foreach ($formData['lead_channels']['subscribed_channels'] as $contactChannel) {
|
||||
if (!isset($leadChannels[$contactChannel])) {
|
||||
$contactable = $dncModel->isContactable($lead, $contactChannel);
|
||||
if (DoNotContact::UNSUBSCRIBED == $contactable || DoNotContact::MANUAL == $contactable) {
|
||||
$dncModel->removeDncForContact($lead->getId(), $contactChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
$dncChannels = array_diff($allChannels, $formData['lead_channels']['subscribed_channels']);
|
||||
foreach ($dncChannels as $channel) {
|
||||
if ($currentChannelId) {
|
||||
$channel = [$channel => $currentChannelId];
|
||||
}
|
||||
$dncModel->addDncForContact($lead->getId(), $channel, ($this->isPublicView) ? DoNotContact::UNSUBSCRIBED : DoNotContact::MANUAL, 'user');
|
||||
}
|
||||
}
|
||||
$leadModel->setFrequencyRules($lead, $formData, $this->leadLists);
|
||||
}
|
||||
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setDoNotContactModel(\Mautic\LeadBundle\Model\DoNotContact $doNotContactModel): void
|
||||
{
|
||||
$this->doNotContactModel = $doNotContactModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name is different, so it won't collide with other setters.
|
||||
*/
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setRequestStackObject(RequestStack $requestStack): void
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\FormBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Event\ImportInitEvent;
|
||||
use Mautic\LeadBundle\Event\ImportMappingEvent;
|
||||
use Mautic\LeadBundle\Event\ImportValidateEvent;
|
||||
use Mautic\LeadBundle\Form\Type\LeadImportFieldType;
|
||||
use Mautic\LeadBundle\Form\Type\LeadImportType;
|
||||
use Mautic\LeadBundle\Helper\Progress;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Model\ImportModel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Form\Exception\LogicException;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
class ImportController extends FormController
|
||||
{
|
||||
// Steps of the import
|
||||
public const STEP_UPLOAD_CSV = 1;
|
||||
|
||||
public const STEP_MATCH_FIELDS = 2;
|
||||
|
||||
public const STEP_PROGRESS_BAR = 3;
|
||||
|
||||
public const STEP_IMPORT_FROM_CSV = 4;
|
||||
|
||||
private ImportModel $importModel;
|
||||
|
||||
public function __construct(
|
||||
FormFactoryInterface $formFactory,
|
||||
FormFieldHelper $fieldHelper,
|
||||
private LoggerInterface $logger,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
private RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
/** @var ImportModel $model */
|
||||
$model = $modelFactory->getModel($this->getModelName());
|
||||
|
||||
$this->importModel = $model;
|
||||
|
||||
parent::__construct($formFactory, $fieldHelper, $doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse
|
||||
*/
|
||||
public function indexAction(Request $request, $page = 1): Response
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$this->requestStack->getSession()->set('mautic.import.object', $initEvent->objectSingular);
|
||||
|
||||
return $this->indexStandard($request, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items for index list.
|
||||
*
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @param mixed[] $filter
|
||||
* @param string $orderBy
|
||||
* @param string $orderByDir
|
||||
* @param mixed[] $args
|
||||
*/
|
||||
protected function getIndexItems($start, $limit, $filter, $orderBy, $orderByDir, array $args = []): array
|
||||
{
|
||||
$object = $this->requestStack->getSession()->get('mautic.import.object');
|
||||
|
||||
$filter['force'][] = [
|
||||
'column' => $this->importModel->getRepository()->getTableAlias().'.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $object,
|
||||
];
|
||||
|
||||
$items = $this->importModel->getEntities(
|
||||
array_merge(
|
||||
[
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
],
|
||||
$args
|
||||
)
|
||||
);
|
||||
|
||||
$count = count($items);
|
||||
|
||||
return [$count, $items];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return array|JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function viewAction(Request $request, $objectId)
|
||||
{
|
||||
return $this->viewStandard($request, $objectId, 'import', 'lead');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel and unpublish the import during manual import.
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse
|
||||
*/
|
||||
public function cancelAction(Request $request): Response
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$object = $initEvent->objectSingular;
|
||||
$fullPath = $this->getFullCsvPath($object);
|
||||
$import = $this->importModel->getEntity($this->requestStack->getSession()->get('mautic.lead.import.id', null));
|
||||
|
||||
if ($import && $import->getId()) {
|
||||
$import->setStatus($import::STOPPED)
|
||||
->setIsPublished(false);
|
||||
$this->importModel->saveEntity($import);
|
||||
}
|
||||
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::INFO, "Import for file {$fullPath} was canceled.");
|
||||
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules manual import to background queue.
|
||||
*/
|
||||
public function queueAction(Request $request): Response
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$object = $initEvent->objectSingular;
|
||||
$fullPath = $this->getFullCsvPath($object);
|
||||
$import = $this->importModel->getEntity($this->requestStack->getSession()->get('mautic.lead.import.id', null));
|
||||
|
||||
if ($import) {
|
||||
$import->setStatus($import::QUEUED);
|
||||
$this->importModel->saveEntity($import);
|
||||
}
|
||||
|
||||
$this->resetImport($object);
|
||||
$this->logger->log(LogLevel::INFO, "Import for file {$fullPath} moved to be processed in the background.");
|
||||
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*/
|
||||
public function newAction(Request $request, $objectId = 0, $ignorePost = false): Response
|
||||
{
|
||||
$dispatcher = $this->dispatcher;
|
||||
|
||||
try {
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
} catch (AccessDeniedException $e) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
if (!$initEvent->objectSupported) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$object = $initEvent->objectSingular;
|
||||
|
||||
$this->requestStack->getSession()->set('mautic.import.object', $object);
|
||||
|
||||
// Move the file to cache and rename it
|
||||
$forceStop = $request->get('cancel', false);
|
||||
$step = ($forceStop) ? self::STEP_UPLOAD_CSV : $this->requestStack->getSession()->get('mautic.'.$object.'.import.step', self::STEP_UPLOAD_CSV);
|
||||
$fileName = $this->getImportFileName($object);
|
||||
$importDir = $this->getImportDirName();
|
||||
$fullPath = $this->getFullCsvPath($object);
|
||||
$fs = new Filesystem();
|
||||
$complete = false;
|
||||
|
||||
if (!file_exists($fullPath) && self::STEP_UPLOAD_CSV !== $step) {
|
||||
// Force step one if the file doesn't exist
|
||||
$this->logger->log(LogLevel::WARNING, "File {$fullPath} does not exist anymore. Reseting import to step STEP_UPLOAD_CSV.");
|
||||
$this->addFlashMessage('mautic.import.file.missing', ['%file%' => $this->getImportFileName($object)], FlashBag::LEVEL_ERROR);
|
||||
$step = self::STEP_UPLOAD_CSV;
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_UPLOAD_CSV);
|
||||
}
|
||||
|
||||
$progress = (new Progress())->bindArray($this->requestStack->getSession()->get('mautic.'.$object.'.import.progress', [0, 0]));
|
||||
$import = $this->importModel->getEntity();
|
||||
$action = $this->generateUrl('mautic_import_action', ['object' => $request->get('object'), 'objectAction' => 'new']);
|
||||
|
||||
switch ($step) {
|
||||
case self::STEP_UPLOAD_CSV:
|
||||
if ($forceStop) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::WARNING, "Import for file {$fullPath} was force-stopped.");
|
||||
}
|
||||
|
||||
$form = $this->formFactory->create(LeadImportType::class, [], ['action' => $action]);
|
||||
break;
|
||||
case self::STEP_MATCH_FIELDS:
|
||||
$mappingEvent = $dispatcher->dispatch(
|
||||
new ImportMappingEvent($request->get('object')),
|
||||
LeadEvents::IMPORT_ON_FIELD_MAPPING
|
||||
);
|
||||
|
||||
try {
|
||||
$form = $this->formFactory->create(
|
||||
LeadImportFieldType::class,
|
||||
[],
|
||||
[
|
||||
'object' => $object,
|
||||
'action' => $action,
|
||||
'all_fields' => $mappingEvent->fields,
|
||||
'import_fields' => $this->requestStack->getSession()->get('mautic.'.$object.'.import.importfields', []),
|
||||
'line_count_limit' => $this->getLineCountLimit(),
|
||||
]
|
||||
);
|
||||
} catch (LogicException $e) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::INFO, "Import for file {$fullPath} failed with: {$e->getMessage()}.");
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
|
||||
break;
|
||||
case self::STEP_PROGRESS_BAR:
|
||||
// Just show the progress form
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_IMPORT_FROM_CSV);
|
||||
break;
|
||||
|
||||
case self::STEP_IMPORT_FROM_CSV:
|
||||
ignore_user_abort(true);
|
||||
|
||||
$inProgress = $this->requestStack->getSession()->get('mautic.'.$object.'.import.inprogress', false);
|
||||
$checks = $this->requestStack->getSession()->get('mautic.'.$object.'.import.progresschecks', 1);
|
||||
if (!$inProgress || $checks > 5) {
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.inprogress', true);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progresschecks', 1);
|
||||
|
||||
$import = $this->importModel->getEntity($this->requestStack->getSession()->get('mautic.'.$object.'.import.id', null));
|
||||
|
||||
if (!$import->getDateStarted()) {
|
||||
$import->setDateStarted(new \DateTime());
|
||||
}
|
||||
|
||||
$this->importModel->process($import, $progress);
|
||||
|
||||
// Clear in progress
|
||||
if ($progress->isFinished()) {
|
||||
$import->setStatus($import::IMPORTED)
|
||||
->setDateEnded(new \DateTime());
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$complete = true;
|
||||
} else {
|
||||
$complete = false;
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.inprogress', false);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progress', $progress->toArray());
|
||||
}
|
||||
|
||||
$this->importModel->saveEntity($import);
|
||||
|
||||
break;
|
||||
} else {
|
||||
++$checks;
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progresschecks', $checks);
|
||||
}
|
||||
}
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $request->getMethod()) {
|
||||
if (!isset($form) || $this->isFormCancelled($form)) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$reason = isset($form) ? 'the form is empty' : 'the form was canceled';
|
||||
$this->logger->log(LogLevel::WARNING, "Import for file {$fullPath} was aborted because {$reason}.");
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
|
||||
$valid = $this->isFormValid($form);
|
||||
switch ($step) {
|
||||
case self::STEP_UPLOAD_CSV:
|
||||
if ($valid) {
|
||||
if (file_exists($fullPath)) {
|
||||
unlink($fullPath);
|
||||
}
|
||||
|
||||
$fileData = $form['file']->getData();
|
||||
if (!empty($fileData)) {
|
||||
$errorMessage = null;
|
||||
$errorParameters = [];
|
||||
try {
|
||||
// Create the import dir recursively
|
||||
$fs->mkdir($importDir);
|
||||
|
||||
$fileData->move($importDir, $fileName);
|
||||
|
||||
$file = new \SplFileObject($fullPath);
|
||||
|
||||
$config = $form->getData();
|
||||
unset($config['file']);
|
||||
unset($config['start']);
|
||||
|
||||
foreach ($config as $key => &$c) {
|
||||
$c = htmlspecialchars_decode($c);
|
||||
|
||||
if ('batchlimit' == $key) {
|
||||
$c = (int) $c;
|
||||
}
|
||||
}
|
||||
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.config', $config);
|
||||
|
||||
// Get the headers for matching
|
||||
$headers = $file->fgetcsv($config['delimiter'], $config['enclosure'], $config['escape']);
|
||||
|
||||
// Get the number of lines so we can track progress
|
||||
$file->seek(PHP_INT_MAX);
|
||||
$linecount = $file->key();
|
||||
|
||||
if (!empty($headers) && is_array($headers)) {
|
||||
$headers = CsvHelper::sanitizeHeaders($headers);
|
||||
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.headers', $headers);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_MATCH_FIELDS);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.importfields', CsvHelper::convertHeadersIntoFields($headers));
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progress', [0, $linecount]);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.original.file', $fileData->getClientOriginalName());
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
} catch (FileException $e) {
|
||||
if (str_contains($e->getMessage(), 'upload_max_filesize')) {
|
||||
$errorMessage = 'mautic.lead.import.filetoolarge';
|
||||
$errorParameters = [
|
||||
'%upload_max_filesize%' => ini_get('upload_max_filesize'),
|
||||
];
|
||||
} else {
|
||||
$errorMessage = 'mautic.lead.import.filenotreadable';
|
||||
}
|
||||
} catch (\Exception) {
|
||||
$errorMessage = 'mautic.lead.import.filenotreadable';
|
||||
} finally {
|
||||
if (!is_null($errorMessage)) {
|
||||
$form->addError(
|
||||
new FormError(
|
||||
$this->translator->trans($errorMessage, $errorParameters, 'validators')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::STEP_MATCH_FIELDS:
|
||||
$validateEvent = new ImportValidateEvent($request->get('object'), $form);
|
||||
|
||||
$dispatcher->dispatch($validateEvent, LeadEvents::IMPORT_ON_VALIDATE);
|
||||
|
||||
if ($validateEvent->hasErrors()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$matchedFields = $validateEvent->getMatchedFields();
|
||||
|
||||
if (empty($matchedFields)) {
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::WARNING, "Import for file {$fullPath} was aborted as there were no matched files found.");
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
}
|
||||
|
||||
/** @var Import $import */
|
||||
$import = $this->importModel->getEntity();
|
||||
|
||||
$import->setMatchedFields($matchedFields)
|
||||
->setObject($object)
|
||||
->setDir($importDir)
|
||||
->setLineCount($this->getLineCount($object))
|
||||
->setFile($fileName)
|
||||
->setOriginalFile($this->requestStack->getSession()->get('mautic.'.$object.'.import.original.file'))
|
||||
->setDefault('owner', $validateEvent->getOwnerId())
|
||||
->setDefault('list', $validateEvent->getList())
|
||||
->setDefault('tags', $validateEvent->getTags())
|
||||
->setDefault('skip_if_exists', $validateEvent->getSkipIfExists())
|
||||
->setHeaders($this->requestStack->getSession()->get('mautic.'.$object.'.import.headers'))
|
||||
->setParserConfig($this->requestStack->getSession()->get('mautic.'.$object.'.import.config'));
|
||||
|
||||
$successMessage = 'mautic.lead.batch.import.created';
|
||||
if (!$this->security->isGranted($this->getPermissionBase().':publish')) {
|
||||
$import->setIsPublished(false);
|
||||
$successMessage = 'mautic.lead.batch.import.created.unpublished';
|
||||
}
|
||||
|
||||
// In case the user chose to import in browser
|
||||
if ($this->importInBrowser($form, $object)) {
|
||||
$import->setStatus($import::MANUAL);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_PROGRESS_BAR);
|
||||
}
|
||||
$this->importModel->saveEntity($import);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.id', $import->getId());
|
||||
// In case the user decided to queue the import
|
||||
if ($this->importInCli($form, $object)) {
|
||||
$this->addFlashMessage($successMessage);
|
||||
$this->resetImport($object);
|
||||
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
return $this->newAction($request, 0, true);
|
||||
default:
|
||||
// Done or something wrong
|
||||
|
||||
$this->resetImport($object);
|
||||
$this->removeImportFile($fullPath);
|
||||
$this->logger->log(LogLevel::ERROR, "Import for file {$fullPath} was aborted for unknown step of '{$step}'");
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (self::STEP_UPLOAD_CSV === $step || self::STEP_MATCH_FIELDS === $step) {
|
||||
$contentTemplate = '@MauticLead/Import/new.html.twig';
|
||||
$viewParameters = [
|
||||
'form' => $form->createView(),
|
||||
'objectName' => $initEvent->objectName,
|
||||
];
|
||||
} else {
|
||||
$contentTemplate = '@MauticLead/Import/progress.html.twig';
|
||||
$viewParameters = [
|
||||
'progress' => $progress,
|
||||
'import' => $import,
|
||||
'complete' => $complete,
|
||||
'failedRows' => $this->importModel->getFailedRows($import->getId(), $import->getObject()),
|
||||
'objectName' => $initEvent->objectName,
|
||||
'indexRoute' => $initEvent->indexRoute,
|
||||
'indexRouteParams' => $initEvent->indexRouteParams,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$complete && $request->query->has('importbatch')) {
|
||||
// Ajax request to batch process so just return ajax response unless complete
|
||||
|
||||
$response = new JsonResponse(['success' => 1, 'ignore_wdt' => 1]);
|
||||
} else {
|
||||
$viewParameters['step'] = $step;
|
||||
|
||||
$response = $this->delegateView(
|
||||
[
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $contentTemplate,
|
||||
'passthroughVars' => [
|
||||
'activeLink' => $initEvent->activeLink,
|
||||
'mauticContent' => 'leadImport',
|
||||
'route' => $this->generateUrl(
|
||||
'mautic_import_action',
|
||||
[
|
||||
'object' => $initEvent->routeObjectName,
|
||||
'objectAction' => 'new',
|
||||
]
|
||||
),
|
||||
'step' => $step,
|
||||
'progress' => $progress,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
// For uploading file Keep-Alive should not be used.
|
||||
$response->headers->set('Connection', 'close');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line count from the session.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getLineCount($object)
|
||||
{
|
||||
$progress = $this->requestStack->getSession()->get('mautic.'.$object.'.import.progress', [0, 0]);
|
||||
|
||||
return $progress[1] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether the import will be processed in client's browser.
|
||||
*
|
||||
* @param FormInterface<mixed> $form
|
||||
* @param string $object
|
||||
*/
|
||||
protected function importInBrowser(FormInterface $form, $object): bool
|
||||
{
|
||||
$browserImportLimit = $this->getLineCountLimit();
|
||||
|
||||
if ($browserImportLimit && $this->getLineCount($object) < $browserImportLimit) {
|
||||
return true;
|
||||
} elseif (!$browserImportLimit && $this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getLineCountLimit()
|
||||
{
|
||||
return $this->coreParametersHelper->get('background_import_if_more_rows_than', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether the import will be queued to be processed by the CLI command in the background.
|
||||
*
|
||||
* @param FormInterface<mixed> $form
|
||||
* @param string $object
|
||||
*/
|
||||
protected function importInCli(FormInterface $form, $object): bool
|
||||
{
|
||||
$browserImportLimit = $this->getLineCountLimit();
|
||||
|
||||
if ($browserImportLimit && $this->getLineCount($object) >= $browserImportLimit) {
|
||||
return true;
|
||||
} elseif (!$browserImportLimit && $this->getFormButton($form, ['buttons', 'apply'])->isClicked()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates import directory path.
|
||||
*/
|
||||
protected function getImportDirName(): string
|
||||
{
|
||||
return $this->importModel->getImportDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates unique import directory name inside the cache dir if not stored in the session.
|
||||
* If it exists in the session, returns that one.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getImportFileName($object)
|
||||
{
|
||||
// Return the dir path from session if exists
|
||||
if ($fileName = $this->requestStack->getSession()->get('mautic.'.$object.'.import.file')) {
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
$fileName = $this->importModel->getUniqueFileName();
|
||||
|
||||
// Set the dir path to session
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.file', $fileName);
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return full absolute path to the CSV file.
|
||||
*
|
||||
* @param string $object
|
||||
*/
|
||||
protected function getFullCsvPath($object): string
|
||||
{
|
||||
return $this->getImportDirName().'/'.$this->getImportFileName($object);
|
||||
}
|
||||
|
||||
private function resetImport(string $object): void
|
||||
{
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.headers', []);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.file', null);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.step', self::STEP_UPLOAD_CSV);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.progress', [0, 0]);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.inprogress', false);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.importfields', []);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.original.file', null);
|
||||
$this->requestStack->getSession()->set('mautic.'.$object.'.import.id', null);
|
||||
}
|
||||
|
||||
private function removeImportFile(string $filepath): void
|
||||
{
|
||||
if (file_exists($filepath) && is_readable($filepath)) {
|
||||
unlink($filepath);
|
||||
|
||||
$this->logger->log(LogLevel::WARNING, "File {$filepath} was removed.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getViewArguments(array $args, $action): array
|
||||
{
|
||||
switch ($action) {
|
||||
case 'view':
|
||||
/** @var Import $entity */
|
||||
$entity = $args['entity'];
|
||||
|
||||
$args['viewParameters'] = array_merge(
|
||||
$args['viewParameters'],
|
||||
[
|
||||
'failedRows' => $this->importModel->getFailedRows($entity->getId(), $entity->getObject()),
|
||||
'importedRowsChart' => $entity->getDateStarted() ? $this->importModel->getImportedRowsLineChartData(
|
||||
'i',
|
||||
$entity->getDateStarted(),
|
||||
$entity->getDateEnded() ?: $entity->getDateModified(),
|
||||
null,
|
||||
[
|
||||
'object_id' => $entity->getId(),
|
||||
]
|
||||
) : [],
|
||||
]
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Support non-index pages such as modal forms.
|
||||
*/
|
||||
protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
||||
{
|
||||
if (!isset($parameters['object'])) {
|
||||
$request = $this->getCurrentRequest();
|
||||
$parameters['object'] = $request->get('object', 'contacts');
|
||||
}
|
||||
|
||||
return parent::generateUrl($route, $parameters, $referenceType);
|
||||
}
|
||||
|
||||
protected function getModelName(): string
|
||||
{
|
||||
return 'lead.import';
|
||||
}
|
||||
|
||||
protected function getSessionBase($objectId = null): string
|
||||
{
|
||||
$initEvent = $this->dispatchImportOnInit();
|
||||
$object = $initEvent->objectSingular;
|
||||
|
||||
return $object.'.import'.(($objectId) ? '.'.$objectId : '');
|
||||
}
|
||||
|
||||
protected function getPermissionBase()
|
||||
{
|
||||
return $this->getModel($this->getModelName())->getPermissionBase();
|
||||
}
|
||||
|
||||
protected function getRouteBase(): string
|
||||
{
|
||||
return 'import';
|
||||
}
|
||||
|
||||
protected function getTemplateBase(): string
|
||||
{
|
||||
return '@MauticLead/Import';
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the name of the column which is used for default ordering.
|
||||
*/
|
||||
protected function getDefaultOrderColumn(): string
|
||||
{
|
||||
return 'dateAdded';
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the direction for default ordering.
|
||||
*/
|
||||
protected function getDefaultOrderDirection(): string
|
||||
{
|
||||
return 'DESC';
|
||||
}
|
||||
|
||||
private function dispatchImportOnInit(): ImportInitEvent
|
||||
{
|
||||
$request = $this->getCurrentRequest();
|
||||
$event = new ImportInitEvent($request->get('object'));
|
||||
|
||||
$this->dispatcher->dispatch($event, LeadEvents::IMPORT_ON_INITIALIZE);
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
trait LeadAccessTrait
|
||||
{
|
||||
/**
|
||||
* Determines if the user has access to the lead the note is for.
|
||||
*
|
||||
* @param bool $isPlugin
|
||||
*
|
||||
* @return Response|Lead
|
||||
*/
|
||||
protected function checkLeadAccess($leadId, $action, $isPlugin = false, $integration = '')
|
||||
{
|
||||
if (!$leadId instanceof Lead) {
|
||||
// make sure the user has view access to this lead
|
||||
$leadModel = $this->getModel('lead');
|
||||
$lead = $leadModel->getEntity((int) $leadId);
|
||||
} else {
|
||||
$lead = $leadId;
|
||||
$leadId = $lead->getId();
|
||||
}
|
||||
|
||||
if (null === $lead || !$lead->getId()) {
|
||||
if (method_exists($this, 'postActionRedirect')) {
|
||||
// set the return URL
|
||||
$page = $this->getCurrentRequest()->getSession()->get($isPlugin ? 'mautic.'.$integration.'.page' : 'mautic.lead.page', 1);
|
||||
$returnUrl = $this->generateUrl($isPlugin ? 'mautic_plugin_timeline_index' : 'mautic_contact_index', ['page' => $page]);
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => $isPlugin ? 'Mautic\LeadBundle\Controller\LeadController::pluginIndexAction' : 'Mautic\LeadBundle\Controller\LeadController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => $isPlugin ? '#mautic_plugin_timeline_index' : '#mautic_contact_index',
|
||||
'mauticContent' => 'leadTimeline',
|
||||
],
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.lead.lead.error.notfound',
|
||||
'msgVars' => ['%id%' => $leadId],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
return $this->notFound('mautic.contact.error.notfound');
|
||||
}
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'lead:leads:'.$action.'own',
|
||||
'lead:leads:'.$action.'other',
|
||||
$lead->getPermissionUser()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
} else {
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns leads the user has access to.
|
||||
*
|
||||
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
protected function checkAllAccess($action, $limit)
|
||||
{
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
|
||||
// make sure the user has view access to leads
|
||||
$repo = $model->getRepository();
|
||||
|
||||
// order by lastactive, filter
|
||||
$leads = $repo->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'l.date_identified',
|
||||
'expr' => 'isNotNull',
|
||||
],
|
||||
],
|
||||
],
|
||||
'oderBy' => 'r.last_active',
|
||||
'orderByDir' => 'DESC',
|
||||
'limit' => $limit,
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
]);
|
||||
|
||||
if (null === $leads) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
if (!$this->security->hasEntityAccess(
|
||||
'lead:leads:'.$action.'own',
|
||||
'lead:leads:'.$action.'other',
|
||||
$lead->getOwner()
|
||||
)
|
||||
) {
|
||||
unset($lead);
|
||||
}
|
||||
}
|
||||
|
||||
return $leads;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Entity\AuditLogRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
trait LeadDetailsTrait
|
||||
{
|
||||
private ?RequestStack $requestStack = null;
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*/
|
||||
protected function getAllEngagements(array $leads, ?array $filters = null, ?array $orderBy = null, $page = 1, $limit = 25): array
|
||||
{
|
||||
$session = $this->requestStack->getCurrentRequest()->getSession();
|
||||
|
||||
if (null == $filters) {
|
||||
$filters = $session->get(
|
||||
'mautic.plugin.timeline.filters',
|
||||
[
|
||||
'search' => '',
|
||||
'includeEvents' => [],
|
||||
'excludeEvents' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (null == $orderBy) {
|
||||
if (!$session->has('mautic.plugin.timeline.orderby')) {
|
||||
$session->set('mautic.plugin.timeline.orderby', 'timestamp');
|
||||
$session->set('mautic.plugin.timeline.orderbydir', 'DESC');
|
||||
}
|
||||
|
||||
$orderBy = [
|
||||
$session->get('mautic.plugin.timeline.orderby'),
|
||||
$session->get('mautic.plugin.timeline.orderbydir'),
|
||||
];
|
||||
}
|
||||
|
||||
// prepare result object
|
||||
$result = [
|
||||
'events' => [],
|
||||
'filters' => $filters,
|
||||
'order' => $orderBy,
|
||||
'types' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'maxPages' => 0,
|
||||
];
|
||||
|
||||
// get events for each contact
|
||||
foreach ($leads as $lead) {
|
||||
// if (!$lead->getEmail()) continue; // discard contacts without email
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
$engagements = $model->getEngagements($lead, $filters, $orderBy, $page, $limit);
|
||||
$events = $engagements['events'];
|
||||
$types = $engagements['types'];
|
||||
|
||||
// inject lead into events
|
||||
foreach ($events as &$event) {
|
||||
$event['leadId'] = $lead->getId();
|
||||
$event['leadEmail'] = $lead->getEmail();
|
||||
$event['leadName'] = $lead->getName() ?: $lead->getEmail();
|
||||
}
|
||||
|
||||
$result['events'] = array_merge($result['events'], $events);
|
||||
$result['types'] = array_merge($result['types'], $types);
|
||||
$result['total'] += $engagements['total'];
|
||||
}
|
||||
|
||||
$result['maxPages'] = ($limit <= 0) ? 1 : round(ceil($result['total'] / $limit));
|
||||
|
||||
usort($result['events'], [$this, 'cmp']); // sort events by
|
||||
|
||||
// now all events are merged, let's limit to $limit
|
||||
array_splice($result['events'], $limit);
|
||||
|
||||
$result['total'] = count($result['events']);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that the event filter array is in the right format.
|
||||
*
|
||||
* @param mixed $filters
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws InvalidArgumentException if not an array
|
||||
*/
|
||||
public function sanitizeEventFilter($filters)
|
||||
{
|
||||
if (!is_array($filters)) {
|
||||
throw new \InvalidArgumentException('filters parameter must be an array');
|
||||
}
|
||||
|
||||
if (!isset($filters['search'])) {
|
||||
$filters['search'] = '';
|
||||
}
|
||||
|
||||
if (!isset($filters['includeEvents'])) {
|
||||
$filters['includeEvents'] = [];
|
||||
}
|
||||
|
||||
if (!isset($filters['excludeEvents'])) {
|
||||
$filters['excludeEvents'] = [];
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
private function cmp($a, $b): int
|
||||
{
|
||||
return $b['timestamp'] <=> $a['timestamp'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of places for the lead based on IP location.
|
||||
*/
|
||||
protected function getPlaces(Lead $lead): array
|
||||
{
|
||||
// Get Places from IP addresses
|
||||
$places = [];
|
||||
if ($lead->getIpAddresses()->count() > 0) {
|
||||
foreach ($lead->getIpAddresses() as $ip) {
|
||||
if ($details = $ip->getIpDetails()) {
|
||||
if (!empty($details['latitude']) && !empty($details['longitude'])) {
|
||||
$name = 'N/A';
|
||||
if (!empty($details['city'])) {
|
||||
$name = $details['city'];
|
||||
} elseif (!empty($details['region'])) {
|
||||
$name = $details['region'];
|
||||
}
|
||||
$place = [
|
||||
'latLng' => [$details['latitude'], $details['longitude']],
|
||||
'name' => $name,
|
||||
];
|
||||
$places[] = $place;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getEngagementData(Lead $lead, ?\DateTime $fromDate = null, ?\DateTime $toDate = null): array
|
||||
{
|
||||
$translator = $this->translator;
|
||||
|
||||
if (null == $fromDate) {
|
||||
$fromDate = new \DateTime('first day of this month 00:00:00');
|
||||
$fromDate->modify('-6 months');
|
||||
}
|
||||
if (null == $toDate) {
|
||||
$toDate = new \DateTime();
|
||||
}
|
||||
|
||||
$lineChart = new LineChart(null, $fromDate, $toDate);
|
||||
$chartQuery = new ChartQuery($this->doctrine->getConnection(), $fromDate, $toDate);
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
$engagements = $model->getEngagementCount($lead, $fromDate, $toDate, 'm', $chartQuery);
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.all_engagements'), $engagements['byUnit']);
|
||||
|
||||
$pointStats = $chartQuery->fetchSumTimeData('lead_points_change_log', 'date_added', ['lead_id' => $lead->getId()], 'delta');
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.points'), $pointStats);
|
||||
|
||||
return $lineChart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function getAuditlogs(Lead $lead, ?array $filters = null, ?array $orderBy = null, int $page = 1, int $limit = 25): array
|
||||
{
|
||||
$session = $this->requestStack->getCurrentRequest()->getSession();
|
||||
|
||||
if (null == $filters) {
|
||||
$filters = $session->get(
|
||||
'mautic.lead.'.$lead->getId().'.auditlog.filters',
|
||||
[
|
||||
'search' => '',
|
||||
'includeEvents' => [],
|
||||
'excludeEvents' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (null == $orderBy) {
|
||||
if (!$session->has('mautic.lead.'.$lead->getId().'.auditlog.orderby')) {
|
||||
$session->set('mautic.lead.'.$lead->getId().'.auditlog.orderby', 'al.dateAdded');
|
||||
$session->set('mautic.lead.'.$lead->getId().'.auditlog.orderbydir', 'DESC');
|
||||
}
|
||||
|
||||
$orderBy = [
|
||||
$session->get('mautic.lead.'.$lead->getId().'.auditlog.orderby'),
|
||||
$session->get('mautic.lead.'.$lead->getId().'.auditlog.orderbydir'),
|
||||
];
|
||||
}
|
||||
|
||||
// Audit Log
|
||||
/** @var AuditLogModel $auditlogModel */
|
||||
$auditlogModel = $this->getModel('core.auditlog');
|
||||
/** @var AuditLogRepository $repo */
|
||||
$repo = $auditlogModel->getRepository();
|
||||
$logCount = $repo->getAuditLogsCount($lead, $filters);
|
||||
$logs = $repo->getAuditLogs($lead, $filters, $orderBy, $page, $limit);
|
||||
|
||||
$logEvents = array_map(fn ($l): array => [
|
||||
'eventType' => $l['action'],
|
||||
'eventLabel' => $l['userName'],
|
||||
'timestamp' => $l['dateAdded'],
|
||||
'details' => $l['details'],
|
||||
'contentTemplate' => '@MauticLead/Auditlog/details.html.twig',
|
||||
], $logs);
|
||||
|
||||
$types = [
|
||||
'delete' => $this->translator->trans('mautic.lead.event.delete'),
|
||||
'create' => $this->translator->trans('mautic.lead.event.create'),
|
||||
'identified' => $this->translator->trans('mautic.lead.event.identified'),
|
||||
'ipadded' => $this->translator->trans('mautic.lead.event.ipadded'),
|
||||
'merge' => $this->translator->trans('mautic.lead.event.merge'),
|
||||
'update' => $this->translator->trans('mautic.lead.event.update'),
|
||||
];
|
||||
|
||||
return [
|
||||
'events' => $logEvents,
|
||||
'filters' => $filters,
|
||||
'order' => $orderBy,
|
||||
'types' => $types,
|
||||
'total' => $logCount,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'maxPages' => ceil($logCount / $limit),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
* @param int $limit
|
||||
*/
|
||||
protected function getEngagements(Lead $lead, ?array $filters = null, ?array $orderBy = null, $page = 1, $limit = 25): array
|
||||
{
|
||||
$session = $this->requestStack->getCurrentRequest()->getSession();
|
||||
|
||||
if (null == $filters) {
|
||||
$filters = $session->get(
|
||||
'mautic.lead.'.$lead->getId().'.timeline.filters',
|
||||
[
|
||||
'search' => '',
|
||||
'includeEvents' => [],
|
||||
'excludeEvents' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (null == $orderBy) {
|
||||
if (!$session->has('mautic.lead.'.$lead->getId().'.timeline.orderby')) {
|
||||
$session->set('mautic.lead.'.$lead->getId().'.timeline.orderby', 'timestamp');
|
||||
$session->set('mautic.lead.'.$lead->getId().'.timeline.orderbydir', 'DESC');
|
||||
}
|
||||
|
||||
$orderBy = [
|
||||
$session->get('mautic.lead.'.$lead->getId().'.timeline.orderby'),
|
||||
$session->get('mautic.lead.'.$lead->getId().'.timeline.orderbydir'),
|
||||
];
|
||||
}
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
|
||||
return $model->getEngagements($lead, $filters, $orderBy, $page, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with engagements and points of a contact.
|
||||
*/
|
||||
protected function getStatsCount(Lead $lead, ?\DateTime $fromDate = null, ?\DateTime $toDate = null): array
|
||||
{
|
||||
if (null == $fromDate) {
|
||||
$fromDate = new \DateTime('first day of this month 00:00:00');
|
||||
$fromDate->modify('-6 months');
|
||||
}
|
||||
if (null == $toDate) {
|
||||
$toDate = new \DateTime();
|
||||
}
|
||||
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead');
|
||||
$chartQuery = new ChartQuery($this->doctrine->getConnection(), $fromDate, $toDate);
|
||||
|
||||
$engagements = $model->getEngagementCount($lead, $fromDate, $toDate, 'm', $chartQuery);
|
||||
$pointStats = $chartQuery->fetchSumTimeData('lead_points_change_log', 'date_added', ['lead_id' => $lead->getId()], 'delta');
|
||||
|
||||
return [
|
||||
'engagements' => $engagements,
|
||||
'points' => $pointStats,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array to create company's engagements graph.
|
||||
*
|
||||
* @param array $contacts
|
||||
*/
|
||||
protected function getCompanyEngagementData($contacts): array
|
||||
{
|
||||
$engagements = [0, 0, 0, 0, 0, 0];
|
||||
$points = [0, 0, 0, 0, 0, 0];
|
||||
foreach ($contacts as $contact) {
|
||||
/** @var LeadModel $model */
|
||||
$model = $this->getModel('lead.lead');
|
||||
|
||||
if (!isset($contact['lead_id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lead = $model->getEntity($contact['lead_id']);
|
||||
|
||||
if (!$lead instanceof Lead) {
|
||||
continue;
|
||||
}
|
||||
$model->getRepository()->refetchEntity($lead);
|
||||
$engagementsData = $this->getStatsCount($lead);
|
||||
|
||||
$engagements = array_map(fn ($a, $b) => $a + $b, $engagementsData['engagements']['byUnit'], $engagements);
|
||||
$points = array_map(fn ($points_first_user, $points_second_user) => $points_first_user + $points_second_user, $engagementsData['points'], $points);
|
||||
}
|
||||
|
||||
return [
|
||||
'engagements' => $engagements,
|
||||
'points' => $points,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company graph for points and engagements.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getCompanyEngagementsForGraph($contacts): array
|
||||
{
|
||||
$graphData = $this->getCompanyEngagementData($contacts);
|
||||
$translator = $this->translator;
|
||||
|
||||
$fromDate = new \DateTime('first day of this month 00:00:00');
|
||||
$fromDate->modify('-6 months');
|
||||
|
||||
$toDate = new \DateTime();
|
||||
|
||||
$lineChart = new LineChart(null, $fromDate, $toDate);
|
||||
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.all_engagements'), $graphData['engagements']);
|
||||
|
||||
$lineChart->setDataset($translator->trans('mautic.lead.graph.line.points'), $graphData['points']);
|
||||
|
||||
return $lineChart->render();
|
||||
}
|
||||
|
||||
protected function getScheduledCampaignEvents(Lead $lead): array
|
||||
{
|
||||
// Upcoming events from Campaign Bundle
|
||||
/** @var \Mautic\CampaignBundle\Entity\LeadEventLogRepository $leadEventLogRepository */
|
||||
$leadEventLogRepository = $this->doctrine->getManager()->getRepository(\Mautic\CampaignBundle\Entity\LeadEventLog::class);
|
||||
|
||||
return $leadEventLogRepository->getUpcomingEvents(
|
||||
[
|
||||
'lead' => $lead,
|
||||
'eventType' => ['action', 'condition'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[\Symfony\Contracts\Service\Attribute\Required]
|
||||
public function setRequestStackLeadDetailsTrait(?RequestStack $requestStack): void
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadNote;
|
||||
use Mautic\LeadBundle\Model\NoteModel;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NoteController extends FormController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* Generate's default list view.
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, $leadId = 0, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
|
||||
// set limits
|
||||
$limit = $session->get(
|
||||
'mautic.lead.'.$lead->getId().'.note.limit',
|
||||
$this->coreParametersHelper->get('default_pagelimit')
|
||||
);
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
$search = $request->get('search', $session->get('mautic.lead.'.$lead->getId().'.note.filter', ''));
|
||||
$session->set('mautic.lead.'.$lead->getId().'.note.filter', $search);
|
||||
|
||||
// do some default filtering
|
||||
$orderBy = $session->get('mautic.lead.'.$lead->getId().'.note.orderby', 'n.dateTime');
|
||||
$orderByDir = $session->get('mautic.lead.'.$lead->getId().'.note.orderbydir', 'DESC');
|
||||
|
||||
$model = $this->getModel('lead.note');
|
||||
$force = [
|
||||
[
|
||||
'column' => 'n.lead',
|
||||
'expr' => 'eq',
|
||||
'value' => $lead,
|
||||
],
|
||||
];
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
$noteType = InputHelper::clean($request->request->all()['noteTypes'] ?? []);
|
||||
if (empty($noteType) && 'index' === $tmpl) {
|
||||
$noteType = $session->get('mautic.lead.'.$lead->getId().'.notetype.filter', []);
|
||||
}
|
||||
$session->set('mautic.lead.'.$lead->getId().'.notetype.filter', $noteType);
|
||||
|
||||
$noteTypes = [
|
||||
'general' => 'mautic.lead.note.type.general',
|
||||
'email' => 'mautic.lead.note.type.email',
|
||||
'call' => 'mautic.lead.note.type.call',
|
||||
'meeting' => 'mautic.lead.note.type.meeting',
|
||||
];
|
||||
|
||||
if (!empty($noteType)) {
|
||||
$force[] = [
|
||||
'column' => 'n.type',
|
||||
'expr' => 'in',
|
||||
'value' => $noteType,
|
||||
];
|
||||
}
|
||||
|
||||
$items = $model->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => $force,
|
||||
'string' => $search,
|
||||
],
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
]
|
||||
);
|
||||
|
||||
$security = $this->security;
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'notes' => $items,
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'search' => $search,
|
||||
'noteType' => $noteType,
|
||||
'noteTypes' => $noteTypes,
|
||||
'tmpl' => $tmpl,
|
||||
'permissions' => [
|
||||
'edit' => $security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser()),
|
||||
'delete' => $security->hasEntityAccess('lead:leads:deleteown', 'lead:leads:deleteown', $lead->getPermissionUser()),
|
||||
],
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'leadNote',
|
||||
'noteCount' => count($items),
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Note/list.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's new note and processes post data.
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, $leadId)
|
||||
{
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
// retrieve the entity
|
||||
$note = new LeadNote();
|
||||
$note->setLead($lead);
|
||||
|
||||
$model = $this->getModel('lead.note');
|
||||
\assert($model instanceof NoteModel);
|
||||
$action = $this->generateUrl(
|
||||
'mautic_contactnote_action',
|
||||
[
|
||||
'objectAction' => 'new',
|
||||
'leadId' => $leadId,
|
||||
]
|
||||
);
|
||||
// get the user form factory
|
||||
$form = $model->createForm($note, $this->formFactory, $action);
|
||||
$closeModal = false;
|
||||
$valid = false;
|
||||
// /Check for a submitted form and process it
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$closeModal = true;
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($note);
|
||||
}
|
||||
} else {
|
||||
$closeModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
$security = $this->security;
|
||||
$permissions = [
|
||||
'edit' => $security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser()),
|
||||
'delete' => $security->hasEntityAccess('lead:leads:deleteown', 'lead:leads:deleteown', $lead->getPermissionUser()),
|
||||
];
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars = [
|
||||
'closeModal' => 1,
|
||||
'mauticContent' => 'leadNote',
|
||||
];
|
||||
|
||||
if ($valid && !$cancelled) {
|
||||
$passthroughVars['upNoteCount'] = 1;
|
||||
$passthroughVars['noteHtml'] = $this->renderView(
|
||||
'@MauticLead/Note/note.html.twig',
|
||||
[
|
||||
'note' => $note,
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
]
|
||||
);
|
||||
$passthroughVars['noteId'] = $note->getId();
|
||||
|
||||
$this->addFlashMessage('mautic.lead.note.created');
|
||||
}
|
||||
|
||||
$passthroughVars['flashes'] = $this->getFlashContent();
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
} else {
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Note/form.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's edit form and processes post data.
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $leadId, $objectId)
|
||||
{
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$model = $this->getModel('lead.note');
|
||||
\assert($model instanceof NoteModel);
|
||||
$note = $model->getEntity($objectId);
|
||||
$closeModal = false;
|
||||
$valid = false;
|
||||
|
||||
if (null === $note || !$this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$action = $this->generateUrl(
|
||||
'mautic_contactnote_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $objectId,
|
||||
'leadId' => $leadId,
|
||||
]
|
||||
);
|
||||
$form = $model->createForm($note, $this->formFactory, $action);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($note);
|
||||
$closeModal = true;
|
||||
}
|
||||
} else {
|
||||
$closeModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
$security = $this->security;
|
||||
$permissions = [
|
||||
'edit' => $security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser()),
|
||||
'delete' => $security->hasEntityAccess('lead:leads:deleteown', 'lead:leads:deleteown', $lead->getPermissionUser()),
|
||||
];
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars['closeModal'] = 1;
|
||||
|
||||
if ($valid && !$cancelled) {
|
||||
$passthroughVars['noteHtml'] = $this->renderView(
|
||||
'@MauticLead/Note/note.html.twig',
|
||||
[
|
||||
'note' => $note,
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
]
|
||||
);
|
||||
$passthroughVars['noteId'] = $note->getId();
|
||||
}
|
||||
|
||||
$passthroughVars['mauticContent'] = 'leadNote';
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
} else {
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'lead' => $lead,
|
||||
'permissions' => $permissions,
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Note/form.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $leadId, $objectId)
|
||||
{
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
$model = $this->getModel('lead.note');
|
||||
\assert($model instanceof NoteModel);
|
||||
$note = $model->getEntity($objectId);
|
||||
|
||||
if (null === $note) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())
|
||||
|| $model->isLocked($note)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$model->deleteEntity($note);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'deleteId' => $objectId,
|
||||
'mauticContent' => 'leadNote',
|
||||
'downNoteCount' => 1,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action defined in route.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param int $leadId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function executeNoteAction(Request $request, $objectAction, $objectId = 0, $leadId = 0)
|
||||
{
|
||||
if (method_exists($this, "{$objectAction}Action")) {
|
||||
return $this->{"{$objectAction}Action"}($request, $leadId, $objectId);
|
||||
} else {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Helper\ExportHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\DateHelper;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TimelineController extends CommonController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
use LeadDetailsTrait;
|
||||
|
||||
public function indexAction(Request $request, $leadId, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.timeline.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderbydir'),
|
||||
];
|
||||
|
||||
$events = $this->getEngagements($lead, $filters, $order, $page);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'events' => $events,
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'leadTimeline',
|
||||
'timelineCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => '@MauticLead/Timeline/_list.html.twig',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function pluginIndexAction(Request $request, $integration, $page = 1)
|
||||
{
|
||||
$limit = 25;
|
||||
$leads = $this->checkAllAccess('view', $limit);
|
||||
|
||||
if ($leads instanceof Response) {
|
||||
return $leads;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' === $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.plugin.timeline.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.plugin.timeline.orderby'),
|
||||
$session->get('mautic.plugin.timeline.orderbydir'),
|
||||
];
|
||||
|
||||
// get all events grouped by lead
|
||||
$events = $this->getAllEngagements($leads, $filters, $order, $page, $limit);
|
||||
|
||||
$str = $request->server->get('QUERY_STRING');
|
||||
parse_str($str, $query);
|
||||
|
||||
$tmpl = 'table';
|
||||
if (array_key_exists('from', $query) && 'iframe' === $query['from']) {
|
||||
$tmpl = 'list';
|
||||
}
|
||||
if (array_key_exists('tmpl', $query)) {
|
||||
$tmpl = $query['tmpl'];
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'leads' => $leads,
|
||||
'page' => $page,
|
||||
'events' => $events,
|
||||
'integration' => $integration,
|
||||
'tmpl' => (!$request->isXmlHttpRequest()) ? 'index' : '',
|
||||
'newCount' => (array_key_exists('count', $query) && $query['count']) ? $query['count'] : 0,
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'pluginTimeline',
|
||||
'timelineCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => sprintf('@MauticLead/Timeline/plugin_%s.html.twig', $tmpl),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function pluginViewAction(Request $request, $integration, $leadId, $page = 1)
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view', true, $integration);
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' === $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.plugin.timeline.'.$leadId.'.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.plugin.timeline.'.$leadId.'.orderby'),
|
||||
$session->get('mautic.plugin.timeline.'.$leadId.'.orderbydir'),
|
||||
];
|
||||
|
||||
$events = $this->getEngagements($lead, $filters, $order, $page);
|
||||
|
||||
$str = $request->server->get('QUERY_STRING');
|
||||
parse_str($str, $query);
|
||||
|
||||
$tmpl = 'table';
|
||||
if (array_key_exists('from', $query) && 'iframe' === $query['from']) {
|
||||
$tmpl = 'list';
|
||||
}
|
||||
if (array_key_exists('tmpl', $query)) {
|
||||
$tmpl = $query['tmpl'];
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'lead' => $lead,
|
||||
'page' => $page,
|
||||
'integration' => $integration,
|
||||
'events' => $events,
|
||||
'newCount' => (array_key_exists('count', $query) && $query['count']) ? $query['count'] : 0,
|
||||
],
|
||||
'passthroughVars' => [
|
||||
'route' => false,
|
||||
'mauticContent' => 'pluginTimeline',
|
||||
'timelineCount' => $events['total'],
|
||||
],
|
||||
'contentTemplate' => sprintf('@MauticLead/Timeline/plugin_%s.html.twig', $tmpl),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function batchExportAction(Request $request, DateHelper $dateHelper, ExportHelper $exportHelper, $leadId): array|Response
|
||||
{
|
||||
if (empty($leadId)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$lead = $this->checkLeadAccess($leadId, 'view');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted('report:export:enable', 'MATCH_ONE')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ('POST' == $request->getMethod() && $request->request->has('search')) {
|
||||
$filters = [
|
||||
'search' => InputHelper::clean($request->request->get('search')),
|
||||
'includeEvents' => InputHelper::clean($request->request->all()['includeEvents'] ?? []),
|
||||
'excludeEvents' => InputHelper::clean($request->request->all()['excludeEvents'] ?? []),
|
||||
];
|
||||
$session->set('mautic.lead.'.$leadId.'.timeline.filters', $filters);
|
||||
} else {
|
||||
$filters = null;
|
||||
}
|
||||
|
||||
$order = [
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderby'),
|
||||
$session->get('mautic.lead.'.$leadId.'.timeline.orderbydir'),
|
||||
];
|
||||
|
||||
$dataType = $request->get('filetype', 'csv');
|
||||
|
||||
$resultsCallback = function ($event) use ($dateHelper): array {
|
||||
$eventLabel = $event['eventLabel'] ?? $event['eventType'];
|
||||
if (is_array($eventLabel)) {
|
||||
$eventLabel = $eventLabel['label'];
|
||||
}
|
||||
|
||||
return [
|
||||
'eventName' => $eventLabel,
|
||||
'eventType' => $event['eventType'] ?? '',
|
||||
'eventTimestamp' => $dateHelper->toText($event['timestamp'], 'local', 'Y-m-d H:i:s', true),
|
||||
];
|
||||
};
|
||||
|
||||
$results = $this->getEngagements($lead, $filters, $order, 1, 200);
|
||||
$count = $results['total'];
|
||||
$items = $results['events'];
|
||||
$iterations = ceil($count / 200);
|
||||
$loop = 1;
|
||||
|
||||
// Max of 50 iterations for 10K result export
|
||||
if ($iterations > 50) {
|
||||
$iterations = 50;
|
||||
}
|
||||
|
||||
$toExport = [];
|
||||
|
||||
while ($loop <= $iterations) {
|
||||
if (is_callable($resultsCallback)) {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = $resultsCallback($item);
|
||||
}
|
||||
} else {
|
||||
foreach ($items as $item) {
|
||||
$toExport[] = (array) $item;
|
||||
}
|
||||
}
|
||||
|
||||
$items = $this->getEngagements($lead, $filters, $order, $loop + 1, 200);
|
||||
|
||||
$this->doctrine->getManager()->clear();
|
||||
|
||||
++$loop;
|
||||
}
|
||||
|
||||
return $this->exportResultsAs($toExport, $dataType, 'contact_timeline', $exportHelper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CategoryBundle\Entity\CategoryRepository;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
|
||||
class LoadCategorizedLeadListData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
/** @var LeadListRepository $leadListRepo */
|
||||
$leadListRepo = $manager->getRepository(LeadList::class);
|
||||
/** @var CategoryRepository $categoryRepo */
|
||||
$categoryRepo = $manager->getRepository(Category::class);
|
||||
|
||||
$leadLists = CsvHelper::csv_to_array(__DIR__.'/fakecategorizedleadlistdata.csv');
|
||||
foreach ($leadLists as $leadList) {
|
||||
$category = $categoryRepo->find($leadList['category']);
|
||||
$leadListEntity = new LeadList();
|
||||
$leadListEntity->setName($leadList['name']);
|
||||
$leadListEntity->setPublicName($leadList['publicname']);
|
||||
$leadListEntity->setAlias($leadList['alias']);
|
||||
$leadListEntity->setCategory($category);
|
||||
$leadListRepo->saveEntity($leadListEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CategoryBundle\Entity\CategoryRepository;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
|
||||
class LoadCategoryData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
/** @var CategoryRepository $categoryRepo */
|
||||
$categoryRepo = $manager->getRepository(Category::class);
|
||||
$categories = CsvHelper::csv_to_array(__DIR__.'/fakecategorydata.csv');
|
||||
foreach ($categories as $category) {
|
||||
$categoryEntity = new Category();
|
||||
$categoryEntity->setTitle($category['categoryname']);
|
||||
$categoryEntity->setBundle($category['categorybundle']);
|
||||
$categoryEntity->setAlias($category['categoryalias']);
|
||||
$categoryEntity->setIsPublished($category['published']);
|
||||
$categoryRepo->saveEntity($categoryEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
|
||||
class LoadCompanyData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CompanyModel $companyModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$today = new \DateTime();
|
||||
$companies = CsvHelper::csv_to_array(__DIR__.'/fakecompanydata.csv');
|
||||
foreach ($companies as $count => $l) {
|
||||
$company = new Company();
|
||||
$company->setDateAdded($today);
|
||||
foreach ($l as $col => $val) {
|
||||
$company->addUpdatedField($col, $val);
|
||||
}
|
||||
$this->companyModel->getRepository()->saveEntity($company);
|
||||
|
||||
$this->setReference('company-'.$count, $company);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\LeadBundle\Entity\CompanyLead;
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
|
||||
class LoadLeadData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
/** @var LeadRepository $leadRepo */
|
||||
$leadRepo = $manager->getRepository(Lead::class);
|
||||
|
||||
/** @var CompanyLeadRepository $companyLeadRepo */
|
||||
$companyLeadRepo = $manager->getRepository(CompanyLead::class);
|
||||
|
||||
$today = new \DateTime();
|
||||
$leads = CsvHelper::csv_to_array(__DIR__.'/fakeleaddata.csv');
|
||||
|
||||
foreach ($leads as $count => $l) {
|
||||
$key = $count + 1;
|
||||
$lead = new Lead();
|
||||
$lead->setDateAdded($today);
|
||||
$ipAddress = new IpAddress();
|
||||
$ipAddress->setIpAddress($l['ip']);
|
||||
$this->setReference('ipAddress-'.$key, $ipAddress);
|
||||
unset($l['ip']);
|
||||
$lead->addIpAddress($ipAddress);
|
||||
|
||||
if ($this->hasReference('sales-user')) {
|
||||
$lead->setOwner($this->getReference('sales-user'));
|
||||
}
|
||||
|
||||
foreach ($l as $col => $val) {
|
||||
$lead->addUpdatedField($col, $val);
|
||||
}
|
||||
|
||||
$leadRepo->saveEntity($lead);
|
||||
|
||||
$this->setReference('lead-'.$count, $lead);
|
||||
|
||||
// Assign to companies in a predictable way
|
||||
$lastCharacter = (int) substr($count, -1, 1);
|
||||
if ($lastCharacter <= 3) {
|
||||
if ($this->hasReference('company-'.$lastCharacter)) {
|
||||
$companyLead = new CompanyLead();
|
||||
$companyLead->setLead($lead);
|
||||
$companyLead->setCompany($this->getReference('company-'.$lastCharacter));
|
||||
$companyLead->setDateAdded($today);
|
||||
$companyLead->setPrimary(true);
|
||||
$companyLeadRepo->saveEntity($companyLead);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
|
||||
class LoadLeadListData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ListModel $segmentModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$adminUser = $this->getReference('admin-user');
|
||||
|
||||
$list = new LeadList();
|
||||
$list->setName('United States');
|
||||
$list->setPublicName('United States');
|
||||
$list->setAlias('us');
|
||||
$list->setCreatedBy($adminUser);
|
||||
$list->setIsGlobal(true);
|
||||
$list->setFilters([
|
||||
[
|
||||
'glue' => 'and',
|
||||
'type' => 'lookup',
|
||||
'field' => 'country',
|
||||
'operator' => '=',
|
||||
'filter' => 'United States',
|
||||
'display' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->setReference('lead-list', $list);
|
||||
$manager->persist($list);
|
||||
$manager->flush();
|
||||
|
||||
$this->segmentModel->rebuildListLeads($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
name,publicname,alias,category
|
||||
Lead List 1 - Segment Category 1,Lead List 1,lead-list-1,1
|
||||
Lead List 2 - Segment Category 2,Lead List 2,lead-list-2,2
|
||||
Lead List 3 - Segment Category 2,Lead List 3,lead-list-3,2
|
||||
Lead List 4 - Segment Category 1,Lead List 4,lead-list-4,1
|
||||
Lead List 5 - Segment Category 1,Lead List 5,lead-list-5,1
|
||||
Lead List 6 - Segment Category 1,Lead List 6,lead-list-6,1
|
||||
Lead List 7 - Segment No Category ,Lead List 6,lead-list-6,
|
||||
|
@@ -0,0 +1,9 @@
|
||||
categoryname,categorybundle,categoryalias,published
|
||||
Segment Test Category 1,segment,segment-test-category-1,1
|
||||
Segment Test Category 2,segment,segment-test-category-2,1
|
||||
Form Test Category 1,form,segment-form-category-1,1
|
||||
Form Test Category 2,form,segment-form-category-2,1
|
||||
Asset Test Category 1,asset,segment-asset-category-1,1
|
||||
Asset Test Category 2,asset,segment-asset-category-2,1
|
||||
Email Test Category 1,email,segment-email-category-1,1
|
||||
Email Test Category 2,email,segment-email-category-2,1
|
||||
|
@@ -0,0 +1,5 @@
|
||||
companyname,companycity,companystate,companycountry,companyindustry
|
||||
Mautic,Boston,Massachusetts,United States,Software
|
||||
Apple,Cupertino,California,United states,Hardware
|
||||
Amazon,Seattle,Washington,United States,Goods
|
||||
HostGator,Houston,Texas,United States,Software
|
||||
|
@@ -0,0 +1,55 @@
|
||||
title,firstname,lastname,city,address1,state,zipcode,country,email,phone,company,website,facebook,twitter,ip,points
|
||||
Mrs.,Penny,Moore,HEWELSFIELD COMMON,88 Clasper Way,,GL15 1XD,United Kingdom,PennyKMoore@dayrep.com,070 7086 0753,Williams Bros.,WikiFire.co.uk,PennyKMoore,PennyKMoore,44.242.120.158,100
|
||||
Mr.,Henry,Catalano,Rustenburg,960 Doreen St,North West,347,South Africa,HenryLCatalano@einrot.com,082 118 9037,Pro Garden Management,MultiFlavors.co.za,HenryLCatalano,HenryLCatalano,83.215.242.109,0
|
||||
Ms.,Stephanie,Cone,PANT,2 Hull Road,,SY10 6ND,United Kingdom,StephanieMCone@teleworm.us,078 4515 7520,Success Is Yours,CampingProfessionals.co.uk,StephanieMCone,StephanieMCone,169.194.102.58,0
|
||||
Mr.,Andrew,Flanagan,WEATHERCOTE,40 Simone Weil Avenue,,LA6 6ZT,United Kingdom,AndrewVFlanagan@dayrep.com,077 6574 7295,William Wanamaker & Sons,CommonFit.co.uk,AndrewVFlanagan,AndrewVFlanagan,12.53.195.22,0
|
||||
Mr.,Daniel,Wright,Qumbu,1084 Bhoola Rd,Eastern Cape,5185,South Africa,DanielAWright@dayrep.com,082 673 3168,Purity Supreme,MysteryShoes.co.za,DanielAWright,DanielAWright,86.210.48.56,0
|
||||
Mr.,Jose,Patton,Ashcroft,727 Mesa Vista Drive,BC,V0K 1A0,Canada,JoseMPatton@jourrapide.com,250-453-4211,Cut Rite,CartridgeExpo.ca,JoseMPatton,JoseMPatton,205.240.234.201,0
|
||||
Dr.,Jean,Cross,WEST CHINNOCK,28 Warren St,,TA18 7XS,United Kingdom,JeanGCross@armyspy.com,077 8114 7167,Superior Appraisals,FirstInstructor.co.uk,JeanGCross,JeanGCross,160.183.198.246,0
|
||||
Mr.,Kevin,Kennedy,COLBROOK,3 Fitzroy Street,VIC,3342,Australia,KevinBKennedy@gustr.com,(03) 5330 2874,Franklin Simon,SlamLounge.com.au,KevinBKennedy,KevinBKennedy,185.58.123.229,0
|
||||
Mr.,Leonard,Sinclair,Cape Town,719 Loop St,Western Cape,7435,South Africa,LeonardMSinclair@teleworm.us,084 524 8203,Thorofare,SizeMedium.co.za,LeonardMSinclair,LeonardMSinclair,46.243.132.129,0
|
||||
Mr.,Bruce,Campbell,MOOLBOOLAMAN,7 Delan Road,QLD,4671,Australia,BruceMCampbell@einrot.com,(07) 3187 6375,Town and Country Convenience Stores,RightWingLunacy.com.au,BruceMCampbell,BruceMCampbell,204.119.237.119,0
|
||||
Ms.,Guadalupe,Strauss,TOOLIBIN,2 Loris Way,WA,6312,Australia,GuadalupeHStrauss@teleworm.us,(08) 9029 4631,Gas Zone,ShowDirectories.com.au,GuadalupeHStrauss,GuadalupeHStrauss,59.85.176.70,0
|
||||
Ms.,Pamela,Wise,WARROCK,25 Normans Road,VIC,3312,Australia,PamelaSWise@gustr.com,(03) 5389 0975,Showbiz Pizza Place,IRCMagazine.com.au,PamelaSWise,PamelaSWise,137.116.91.223,0
|
||||
Ms.,Margaret,Maguire,Laurentides,282 rue des Églises Est,QC,J0R 1C0,Canada,MargaretDMaguire@cuvox.de,450-439-2306,Pender's Food Stores,AmateurCredit.ca,MargaretDMaguire,MargaretDMaguire,72.139.24.22,0
|
||||
Ms.,Regina,Dolph,SOLAS,27 Hounslow Rd,,HS6 2YL,United Kingdom,ReginaBDolph@teleworm.us,077 0685 3094,Record & Tape Outlet,MediumNews.co.uk,ReginaBDolph,ReginaBDolph,125.4.222.31,0
|
||||
Mrs.,Paula,Hill,Brits,2383 South St,North West,220,South Africa,PaulaWHill@dayrep.com,085 488 7773,Alladin's Lamp,DominoRoom.co.za,PaulaWHill,PaulaWHill,187.166.22.117,0
|
||||
Mr.,Jimmy,Sanchez,COQUETTE POINT,90 Boulter Close,QLD,4860,Australia,JimmyCSanchez@dayrep.com,(07) 4042 9552,On Cue,GrandMassage.com.au,JimmyCSanchez,JimmyCSanchez,224.145.91.15,0
|
||||
Ms.,Mildred,Rodriguez,BADGERIN ROCK,17 Muscat Street,WA,6475,Australia,MildredARodriguez@rhyta.com,(08) 9060 4567,Team Electronics,DoubleLimousine.com.au,MildredARodriguez,MildredARodriguez,134.222.144.84,0
|
||||
Mrs.,Kyung,Brittain,MIDDLE BROOK,17 Hart Street,NSW,2337,Australia,KyungBBrittain@dayrep.com,(02) 4997 6844,Dream Home Improvements,BloggerRoom.com.au,KyungBBrittain,KyungBBrittain,86.127.202.144,0
|
||||
Mr.,Willie,Perez,Albuquerque,3683 Byrd Lane,NM,87112,United States,WillieJPerez@jourrapide.com,505-292-1492,Crandall's Fine Furniture,ThermalNetworks.com,WillieJPerez,WillieJPerez,211.124.214.94,0
|
||||
Mr.,Marvin,Patterson,TREFNANNEY,40 Glandovey Terrace,,SY22 2AD,United Kingdom,MarvinPPatterson@jourrapide.com,079 4031 1312,Chi-Chi's,FabulousSeats.co.uk,MarvinPPatterson,MarvinPPatterson,240.51.62.57,0
|
||||
Ms.,Rosemary,Salinas,Brakpan,2131 Hoog St,Gauteng,1544,South Africa,RosemaryKSalinas@superrito.com,083 893 4273,Omni Tech,RegionHotels.co.za,RosemaryKSalinas,RosemaryKSalinas,189.86.78.59,0
|
||||
Mr.,Paul,Wilson,Petit,244 Hoog St,Gauteng,1512,South Africa,PaulDWilson@superrito.com,085 388 8905,Magna Solution,WebDivorces.co.za,PaulDWilson,PaulDWilson,89.46.89.21,0
|
||||
Ms.,Roxie,Shaw,RHAOINE,97 Boat Lane,,IV28 9UH,United Kingdom,RoxieLShaw@fleckens.hu,079 6441 8665,Old America Stores,WifeReplacement.co.uk,RoxieLShaw,RoxieLShaw,211.111.137.180,0
|
||||
Ms.,Angie,Robles,Stoney Creek,2393 Barton Street,ON,L8G 2V1,Canada,AngieHRobles@einrot.com,905-573-0032,Pro Garden Management,EscrowWireless.ca,AngieHRobles,AngieHRobles,2.227.195.136,0
|
||||
Mrs.,Charlotte,Fender,Des Moines,113 Nutters Barn Lane,IA,50313,United States,CharlotteAFender@einrot.com,515-729-9343,CSK Auto,AffordableIncentive.com,CharlotteAFender,CharlotteAFender,194.85.219.26,0
|
||||
Mrs.,Lashawnda,Joseph,LYDFORD-ON-FOSSE,43 South Crescent,,TA11 7UU,United Kingdom,LashawndaDJoseph@gustr.com,070 1704 6116,Tam's Stationers,MakeupDiscounts.co.uk,LashawndaDJoseph,LashawndaDJoseph,211.167.170.168,0
|
||||
Mrs.,Helen,Manley,Creighton,339 Impala St,KwaZulu-Natal,3263,South Africa,HelenPManley@dayrep.com,082 107 6053,Farrell's Ice Cream Parlour,BankingVentures.co.za,HelenPManley,HelenPManley,36.129.7.21,0
|
||||
Ms.,Annie,Richarson,Adrian,40 Amethyst Drive,MI,49221,United States,AnnieARicharson@armyspy.com,517-266-4755,Bodega Club,GolfCleaners.com,AnnieARicharson,AnnieARicharson,179.68.77.113,0
|
||||
Ms.,Mary,Nevarez,AITH,76 Asfordby Rd,,ZE2 4FH,United Kingdom,MaryWNevarez@armyspy.com,077 8292 2559,Builders Square,JollyForum.co.uk,MaryWNevarez,MaryWNevarez,153.154.172.242,0
|
||||
Mr.,David,Fahy,NEW ULVA,28 Lairg Road,,PA31 8WG,United Kingdom,DavidEFahy@dayrep.com,079 7960 8698,Bumper to Bumper Auto Parts,SearchWealth.co.uk,DavidEFahy,DavidEFahy,199.7.8.156,0
|
||||
Mr.,Aaron,Guild,EWLOE,86 Wrexham Rd,,CH5 9LB,United Kingdom,AaronMGuild@rhyta.com,070 8638 9402,Scotty's Builders Supply,MobLag.co.uk,AaronMGuild,AaronMGuild,90.41.142.23,0
|
||||
Mr.,Lee,Cole,Marble Hall,1969 Dikbas Road,Mpumalanga,451,South Africa,LeeACole@fleckens.hu,085 470 6278,Checker Auto Parts,BadProtection.co.za,LeeACole,LeeACole,209.183.6.82,0
|
||||
Mr.,Matthew,Dell,Aberdeen,1087 Hartway Street,SD,57401,United States,MatthewSDell@armyspy.com,605-790-9178,Wealthy Ideas,FireGourd.com,MatthewSDell,MatthewSDell,92.90.197.187,0
|
||||
Ms.,Raquel,O'Sullivan,Windsor,1227 Goyeau Ave,ON,N9A 1H9,Canada,RaquelTOSullivan@gustr.com,519-919-6675,Asian Solutions,RobotMarketing.ca,RaquelTOSullivan,RaquelTOSullivan,38.82.155.239,0
|
||||
Mrs.,Debra,Shackelford,Longueuil,1879 rue Saint-Charles,QC,J4H 1M3,Canada,DebraCShackelford@dayrep.com,450-463-1825,Lum's,BetterSearchTool.ca,DebraCShackelford,DebraCShackelford,93.213.3.76,0
|
||||
Ms.,Marcia,Hibbard,Pritchard,4251 Blind Bay Road,BC,V0E 2P0,Canada,MarciaBHibbard@fleckens.hu,250-577-9200,Stop N Shop,DishRebates.ca,MarciaBHibbard,MarciaBHibbard,52.122.202.53,0
|
||||
Mr.,Thomas,Domingue,Hummelstown,1988 Lincoln Drive,PA,17036,United States,ThomasJDomingue@armyspy.com,717-566-3468,Holly Tree Inn,ThinkingMeds.com,ThomasJDomingue,ThomasJDomingue,235.234.136.88,0
|
||||
Mr.,Jeremy,Newell,Oshtemo,600 Shingleton Road,MI,49077,United States,JeremyJNewell@fleckens.hu,269-372-6669,Leo's Stereo,ZBlvd.com,JeremyJNewell,JeremyJNewell,10.248.239.100,0
|
||||
Mr.,Justin,Waller,Johannesburg,1348 President St,Gauteng,2102,South Africa,JustinRWaller@cuvox.de,083 324 8545,LoRay,SecurityWorkshops.co.za,JustinRWaller,JustinRWaller,34.126.122.243,0
|
||||
Ms.,Brenda,Bolton,Temba,2143 Robertson Ave,North West,505,South Africa,BrendaWBolton@dayrep.com,083 411 4857,Huffman and Boyle,GuyHumor.co.za,BrendaWBolton,BrendaWBolton,54.128.70.146,0
|
||||
Ms.,Renee,Smith,WATTLE RIDGE,44 Railway Street,QLD,4357,Australia,ReneeTSmith@teleworm.us,(07) 4527 1699,Linens 'n Things,ShowFever.com.au,ReneeTSmith,ReneeTSmith,218.13.78.2,0
|
||||
Mr.,David,Cook,Ga-Maraba,1818 Mark Street,Limpopo,705,South Africa,DavidECook@dayrep.com,083 326 3665,Big Star Markets,CreditCardChronicles.co.za,DavidECook,DavidECook,94.232.240.187,0
|
||||
Mrs.,June,Bond,St Barthelemy,1921 chemin Georges,QC,J0K 1X0,Canada,JuneLBond@superrito.com,450-885-5404,Realty Depot,NoteNews.ca,JuneLBond,JuneLBond,190.243.9.242,0
|
||||
Mr.,James,Duffy,Greensboro,3716 Keyser Ridge Road,NC,27401,United States,JamesTDuffy@armyspy.com,336-508-1155,Reliable Guidance,AnonymousMortgage.com,JamesTDuffy,JamesTDuffy,110.211.41.91,0
|
||||
Mr.,Jonathan,Lane,Nelspruit,1408 Ireland St,Mpumalanga,1220,South Africa,JonathanJLane@jourrapide.com,082 917 7446,Life Map,YouBlogs.co.za,JonathanJLane,JonathanJLane,190.176.178.200,0
|
||||
Mr.,Peter,Howard,Vancouver,4514 Robson St,BC,V6B 3K9,Canada,PeterJHoward@armyspy.com,604-916-2079,Payless Cashways,FriendTraders.ca,PeterJHoward,PeterJHoward,169.163.46.203,0
|
||||
Ms.,Irene,Martin,Ottawa,2312 Bank St,ON,K1H 7Z1,Canada,IreneGMartin@cuvox.de,613-818-9603,Helios Air,ForumJet.ca,IreneGMartin,IreneGMartin,34.167.191.88,0
|
||||
Mr.,David,Jameson,MUGINCOBLE,52 Tooraweenah Road,NSW,2870,Australia,DavidLJameson@einrot.com,(02) 4069 4141,House Of Denmark,ModelSolar.com.au,DavidLJameson,DavidLJameson,41.6.233.4,0
|
||||
Mr.,Lewis,Syed,Statesboro,107 Yorkie Lane,GA,30458,United States,LewisTSyed@gustr.com,912-682-3070,Record Bar,BetterSearchTool.com,LewisTSyed,LewisTSyed,18.57.6.112,0
|
||||
Ms.,Nellie,Baird,Port Elizabeth,1930 Uitsig St,Eastern Cape,6204,South Africa,NellieABaird@armyspy.com,083 926 4318,Huyler's,CosmeticsCritic.co.za,NellieABaird,NellieABaird,34.245.44.224,0
|
||||
,,,Port Elizabeth,1930 Uitsig St,Eastern Cape,6204,South Africa,,,,,,,34.245.44.224,0
|
||||
,,,Port Elizabeth,1930 Uitsig St,Eastern Cape,6204,South Africa,,,,,,,34.245.44.224,0
|
||||
,,,Port Elizabeth,1930 Uitsig St,Eastern Cape,6204,South Africa,,,,,,,34.245.44.224,0
|
||||
,,,Port Elizabeth,1930 Uitsig St,Eastern Cape,6204,South Africa,,,,,,,34.245.44.224,0
|
||||
|
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\DataObject;
|
||||
|
||||
use Mautic\CoreBundle\Form\DataTransformer\BarStringTransformer;
|
||||
use Mautic\LeadBundle\Exception\InvalidContactFieldTokenException;
|
||||
|
||||
/**
|
||||
* A value object representation of a contact field token.
|
||||
*/
|
||||
class ContactFieldToken
|
||||
{
|
||||
private string $fieldAlias;
|
||||
|
||||
private ?string $defaultValue;
|
||||
|
||||
/**
|
||||
* @throws InvalidContactFieldTokenException
|
||||
*/
|
||||
public function __construct(
|
||||
private string $fullToken,
|
||||
) {
|
||||
$this->parse(trim($fullToken));
|
||||
}
|
||||
|
||||
public function getFullToken(): string
|
||||
{
|
||||
return $this->fullToken;
|
||||
}
|
||||
|
||||
public function getFieldAlias(): string
|
||||
{
|
||||
return $this->fieldAlias;
|
||||
}
|
||||
|
||||
public function getDefaultValue(): ?string
|
||||
{
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
private function parse(string $fullToken): void
|
||||
{
|
||||
preg_match('/^{contactfield=(.*?)}$/', $fullToken, $matches);
|
||||
|
||||
if (empty($matches[1])) {
|
||||
throw new InvalidContactFieldTokenException("'{$fullToken}' is not a valid contact field token. A valid token example: '{contactfield=firstname|John}'");
|
||||
}
|
||||
|
||||
$barStringTransformer = new BarStringTransformer();
|
||||
$array = $barStringTransformer->reverseTransform($matches[1]);
|
||||
$this->fieldAlias = $array[0];
|
||||
$this->defaultValue = $array[1] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\DataObject;
|
||||
|
||||
class LeadManipulator
|
||||
{
|
||||
/**
|
||||
* If true then the manipulator was logged and should not be logged for the second time.
|
||||
*/
|
||||
private bool $logged = false;
|
||||
|
||||
/**
|
||||
* @param ?string $bundleName
|
||||
* @param ?string $objectName
|
||||
* @param ?int $objectId
|
||||
* @param ?string $objectDescription
|
||||
*/
|
||||
public function __construct(
|
||||
private $bundleName = null,
|
||||
private $objectName = null,
|
||||
private $objectId = null,
|
||||
private $objectDescription = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getBundleName()
|
||||
{
|
||||
return $this->bundleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getObjectName()
|
||||
{
|
||||
return $this->objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?int
|
||||
*/
|
||||
public function getObjectId()
|
||||
{
|
||||
return $this->objectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getObjectDescription()
|
||||
{
|
||||
return $this->objectDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the manipulator was logged already or not.
|
||||
*/
|
||||
public function wasLogged(): bool
|
||||
{
|
||||
return $this->logged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set manipulator as logged so it wouldn't be logged for the second time in the same request.
|
||||
*/
|
||||
public function setAsLogged(): void
|
||||
{
|
||||
$this->logged = true;
|
||||
}
|
||||
|
||||
public function getManipulatedBy(): string
|
||||
{
|
||||
if ($this->objectDescription) {
|
||||
return (string) $this->objectDescription;
|
||||
}
|
||||
|
||||
return $this->getManipulatorKey();
|
||||
}
|
||||
|
||||
public function getManipulatorKey(): string
|
||||
{
|
||||
$objectParts = [];
|
||||
if ($this->bundleName) {
|
||||
$objectParts[] = $this->bundleName;
|
||||
}
|
||||
if ($this->objectName) {
|
||||
$objectParts[] = $this->objectName;
|
||||
}
|
||||
if ($this->objectId) {
|
||||
$objectParts[] = $this->objectId;
|
||||
}
|
||||
|
||||
return implode(':', $objectParts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Deduplicate;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyRepository;
|
||||
use Mautic\LeadBundle\Exception\UniqueFieldNotFoundException;
|
||||
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
|
||||
class CompanyDeduper
|
||||
{
|
||||
use DeduperTrait;
|
||||
|
||||
public function __construct(
|
||||
FieldModel $fieldModel,
|
||||
FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
|
||||
private CompanyRepository $companyRepository,
|
||||
) {
|
||||
$this->fieldModel = $fieldModel;
|
||||
$this->fieldsWithUniqueIdentifier = $fieldsWithUniqueIdentifier;
|
||||
$this->object = 'company';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company[]
|
||||
*
|
||||
* @throws UniqueFieldNotFoundException
|
||||
*/
|
||||
public function checkForDuplicateCompanies(array $queryFields): array
|
||||
{
|
||||
$uniqueData = $this->getUniqueData($queryFields);
|
||||
if (empty($uniqueData)) {
|
||||
throw new UniqueFieldNotFoundException();
|
||||
}
|
||||
|
||||
return $this->companyRepository->getCompaniesByUniqueFields($uniqueData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Deduplicate;
|
||||
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
|
||||
class ContactDeduper
|
||||
{
|
||||
use DeduperTrait;
|
||||
|
||||
public function __construct(
|
||||
FieldModel $fieldModel,
|
||||
FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
|
||||
private ContactMerger $contactMerger,
|
||||
private LeadRepository $leadRepository,
|
||||
) {
|
||||
$this->fieldModel = $fieldModel;
|
||||
$this->fieldsWithUniqueIdentifier = $fieldsWithUniqueIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public function getUniqueFields(string $object): array
|
||||
{
|
||||
return $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier(['object' => $object]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $uniqueFieldAliases
|
||||
*/
|
||||
public function countDuplicatedContacts(array $uniqueFieldAliases): int
|
||||
{
|
||||
return $this->leadRepository->getContactCountWithDuplicateValues($uniqueFieldAliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $uniqueFieldAliases
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDuplicateContactIds(array $uniqueFieldAliases): array
|
||||
{
|
||||
return $this->leadRepository->getDuplicatedContactIds($uniqueFieldAliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|int[] $contactIds
|
||||
*
|
||||
* @return Lead[]
|
||||
*/
|
||||
public function getContactsByIds(array $contactIds): array
|
||||
{
|
||||
return $this->leadRepository->getEntities(['ids' => $contactIds, 'ignore_paginator' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $contacts
|
||||
*/
|
||||
public function deduplicateContactBatch(array $contacts, bool $newerIntoOlder, ?callable $onContactProcessed = null): void
|
||||
{
|
||||
foreach ($contacts as $contact) {
|
||||
$duplicates = $this->checkForDuplicateContacts($contact->getProfileFields(), $newerIntoOlder);
|
||||
|
||||
$this->mergeContacts($duplicates);
|
||||
$this->detachContacts($duplicates);
|
||||
|
||||
if ($onContactProcessed) {
|
||||
$onContactProcessed($contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To save RAM.
|
||||
*
|
||||
* @param Lead[] $contacts
|
||||
*/
|
||||
public function detachContacts(array $contacts): void
|
||||
{
|
||||
$this->leadRepository->detachEntities($contacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $duplicates
|
||||
*/
|
||||
public function mergeContacts(array $duplicates): void
|
||||
{
|
||||
if (empty($duplicates)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loser = reset($duplicates);
|
||||
while ($winner = next($duplicates)) {
|
||||
try {
|
||||
$this->contactMerger->merge($winner, $loser);
|
||||
} catch (SameContactException) {
|
||||
}
|
||||
|
||||
$loser = $winner;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]
|
||||
*/
|
||||
public function checkForDuplicateContacts(array $queryFields, bool $mergeNewerIntoOlder = false)
|
||||
{
|
||||
$duplicates = [];
|
||||
$uniqueData = $this->getUniqueData($queryFields);
|
||||
if (!empty($uniqueData)) {
|
||||
$duplicates = $this->leadRepository->getLeadsByUniqueFields($uniqueData);
|
||||
|
||||
// By default, duplicates are ordered by newest first
|
||||
if (!$mergeNewerIntoOlder) {
|
||||
// Reverse the array so that oldest are on "top" in order to merge oldest into the next until they all have been merged into the
|
||||
// the newest record
|
||||
$duplicates = array_reverse($duplicates);
|
||||
}
|
||||
}
|
||||
|
||||
return $duplicates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Deduplicate;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ArrayHelper;
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\ValueNotMergeableException;
|
||||
use Mautic\LeadBundle\Deduplicate\Helper\MergeValueHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\MergeRecord;
|
||||
use Mautic\LeadBundle\Entity\MergeRecordRepository;
|
||||
use Mautic\LeadBundle\Event\LeadMergeEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class ContactMerger
|
||||
{
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
protected $winner;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
protected $loser;
|
||||
|
||||
public function __construct(
|
||||
protected LeadModel $leadModel,
|
||||
protected MergeRecordRepository $repo,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SameContactException
|
||||
*/
|
||||
public function merge(Lead $winner, Lead $loser): Lead
|
||||
{
|
||||
if ($winner->getId() === $loser->getId()) {
|
||||
throw new SameContactException();
|
||||
}
|
||||
|
||||
$this->logger->debug('CONTACT: ID# '.$loser->getId().' will be merged into ID# '.$winner->getId());
|
||||
|
||||
// Dispatch pre merge event
|
||||
$event = new LeadMergeEvent($winner, $loser);
|
||||
$this->dispatcher->dispatch($event, LeadEvents::LEAD_PRE_MERGE);
|
||||
|
||||
// Merge everything
|
||||
$this->updateMergeRecords($winner, $loser)
|
||||
->mergeTimestamps($winner, $loser)
|
||||
->mergeIpAddressHistory($winner, $loser)
|
||||
->mergeFieldData($winner, $loser)
|
||||
->mergeOwners($winner, $loser)
|
||||
->mergePoints($winner, $loser)
|
||||
->mergeTags($winner, $loser);
|
||||
|
||||
// Save the updated contact
|
||||
$this->leadModel->saveEntity($winner, false);
|
||||
|
||||
// Dispatch post merge event
|
||||
$this->dispatcher->dispatch($event, LeadEvents::LEAD_POST_MERGE);
|
||||
|
||||
// Delete the loser
|
||||
$this->leadModel->deleteEntity($loser);
|
||||
|
||||
return $winner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge timestamps.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function mergeTimestamps(Lead $winner, Lead $loser)
|
||||
{
|
||||
// The winner should keep the most recent last active timestamp of the two
|
||||
if ($loser->getLastActive() > $winner->getLastActive()) {
|
||||
$winner->setLastActive($loser->getLastActive());
|
||||
}
|
||||
|
||||
/*
|
||||
* The winner should keep the oldest date identified timestamp
|
||||
* as long as the loser's date identified is not null.
|
||||
* Alternatively, if the winner's date identified is null,
|
||||
* use the loser's date identified (doesn't matter if it is null).
|
||||
*/
|
||||
if ((null !== $loser->getDateIdentified() && $loser->getDateIdentified() < $winner->getDateIdentified()) || null === $winner->getDateIdentified()) {
|
||||
$winner->setDateIdentified($loser->getDateIdentified());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge IP history into the winner.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function mergeIpAddressHistory(Lead $winner, Lead $loser)
|
||||
{
|
||||
$ipAddresses = $loser->getIpAddresses();
|
||||
|
||||
foreach ($ipAddresses as $ip) {
|
||||
$winner->addIpAddress($ip);
|
||||
|
||||
$this->logger->debug('CONTACT: Associating '.$winner->getId().' with IP '.$ip->getIpAddress());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge custom field data into winner.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function mergeFieldData(Lead $winner, Lead $loser)
|
||||
{
|
||||
// Use the modified date if applicable or date added if the contact has never been edited
|
||||
$loserDate = $loser->getDateModified() ?: $loser->getDateAdded();
|
||||
$winnerDate = $winner->getDateModified() ?: $winner->getDateAdded();
|
||||
|
||||
// When it comes to data, keep the newest value regardless of the winner/loser
|
||||
$newest = ($loserDate > $winnerDate) ? $loser : $winner;
|
||||
$oldest = ($newest->getId() === $winner->getId()) ? $loser : $winner;
|
||||
|
||||
// It may happen that the Lead entities doesn't have fields fill in. Fill them in if not.
|
||||
if (!$newest->hasFields()) {
|
||||
$newest->setFields($this->leadModel->getRepository()->getFieldValues($newest->getId()));
|
||||
}
|
||||
|
||||
if (!$oldest->hasFields()) {
|
||||
$oldest->setFields($this->leadModel->getRepository()->getFieldValues($oldest->getId()));
|
||||
}
|
||||
|
||||
$newestFields = $newest->getProfileFields();
|
||||
$oldestFields = $oldest->getProfileFields();
|
||||
|
||||
foreach (array_keys($newestFields) as $field) {
|
||||
if (in_array($field, ['id', 'points'])) {
|
||||
// Let mergePoints() take care of this
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$fromValue = empty($oldestFields[$field]) ? 'empty' : $oldestFields[$field];
|
||||
$fieldDetails = $winner->getField($field);
|
||||
|
||||
if (false === $fieldDetails) {
|
||||
throw new ValueNotMergeableException($fromValue, false);
|
||||
}
|
||||
|
||||
$defaultValue = ArrayHelper::getValue('default_value', $fieldDetails);
|
||||
$newValue = MergeValueHelper::getMergeValue(
|
||||
$newestFields[$field],
|
||||
$oldestFields[$field],
|
||||
$winner->getFieldValue($field),
|
||||
$defaultValue,
|
||||
$newest->isAnonymous()
|
||||
);
|
||||
$winner->addUpdatedField($field, $newValue);
|
||||
|
||||
$this->logger->debug("CONTACT: Updated {$field} from {$fromValue} to {$newValue} for {$winner->getId()}");
|
||||
} catch (ValueNotMergeableException $exception) {
|
||||
$this->logger->info("CONTACT: {$field} is not mergeable for {$winner->getId()} - {$exception->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge owners if the winner isn't already assigned an owner.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function mergeOwners(Lead $winner, Lead $loser)
|
||||
{
|
||||
$oldOwner = $winner->getOwner();
|
||||
$newOwner = $loser->getOwner();
|
||||
|
||||
if (null === $oldOwner && null !== $newOwner) {
|
||||
$winner->setOwner($newOwner);
|
||||
|
||||
$this->logger->debug("CONTACT: New owner of {$winner->getId()} is {$newOwner->getId()}");
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum points from both contacts.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function mergePoints(Lead $winner, Lead $loser)
|
||||
{
|
||||
$winnerPoints = (int) $winner->getPoints();
|
||||
$loserPoints = (int) $loser->getPoints();
|
||||
$winner->adjustPoints($loserPoints);
|
||||
|
||||
$this->logger->debug(
|
||||
'CONTACT: Adding '.$loserPoints.' points from contact ID #'.$loser->getId().' to contact ID #'.$winner->getId().' with '.$winnerPoints
|
||||
.' points'
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge tags from loser into winner.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function mergeTags(Lead $winner, Lead $loser)
|
||||
{
|
||||
$loserTags = $loser->getTags();
|
||||
$addTags = $loserTags->getKeys();
|
||||
|
||||
$this->leadModel->modifyTags($winner, $addTags, null, false);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge past merge records into the winner.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
private function updateMergeRecords(Lead $winner, Lead $loser)
|
||||
{
|
||||
// Update merge records for the lead about to be deleted
|
||||
$this->repo->moveMergeRecord($loser->getId(), $winner->getId());
|
||||
|
||||
// Create an entry this contact was merged
|
||||
$mergeRecord = new MergeRecord();
|
||||
$mergeRecord->setContact($winner)
|
||||
->setDateAdded()
|
||||
->setName($loser->getPrimaryIdentifier())
|
||||
->setMergedId($loser->getId());
|
||||
|
||||
$this->repo->saveEntity($mergeRecord);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Deduplicate;
|
||||
|
||||
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
|
||||
trait DeduperTrait
|
||||
{
|
||||
private $object = 'lead';
|
||||
|
||||
/**
|
||||
* @var FieldModel
|
||||
*/
|
||||
private $fieldModel;
|
||||
|
||||
private FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $availableFields;
|
||||
|
||||
public function getUniqueData(array $queryFields): array
|
||||
{
|
||||
$uniqueLeadFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier(['object' => $this->object]);
|
||||
$uniqueLeadFieldData = [];
|
||||
$inQuery = array_intersect_key($queryFields, $this->getAvailableFields());
|
||||
foreach ($inQuery as $k => $v) {
|
||||
// Don't use empty values when checking for duplicates
|
||||
if (empty($v)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists($k, $uniqueLeadFields)) {
|
||||
$uniqueLeadFieldData[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $uniqueLeadFieldData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getAvailableFields()
|
||||
{
|
||||
if (null === $this->availableFields) {
|
||||
$this->availableFields = $this->fieldModel->getFieldList(
|
||||
false,
|
||||
false,
|
||||
[
|
||||
'isPublished' => true,
|
||||
'object' => $this->object,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $this->availableFields;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Deduplicate\Exception;
|
||||
|
||||
class SameContactException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Deduplicate\Exception;
|
||||
|
||||
class ValueNotMergeableException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @param mixed $newerValue
|
||||
* @param mixed $olderValue
|
||||
*/
|
||||
public function __construct(
|
||||
private $newerValue,
|
||||
private $olderValue,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getNewerValue()
|
||||
{
|
||||
return $this->newerValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOlderValue()
|
||||
{
|
||||
return $this->olderValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Deduplicate\Helper;
|
||||
|
||||
use Mautic\LeadBundle\Deduplicate\Exception\ValueNotMergeableException;
|
||||
|
||||
class MergeValueHelper
|
||||
{
|
||||
/**
|
||||
* @param mixed $newerValue
|
||||
* @param mixed $olderValue
|
||||
* @param mixed $currentValue
|
||||
* @param mixed $defaultValue
|
||||
* @param bool $newIsAnonymous
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws ValueNotMergeableException
|
||||
*/
|
||||
public static function getMergeValue($newerValue, $olderValue, $currentValue = null, $defaultValue = null, $newIsAnonymous = false)
|
||||
{
|
||||
if ($newerValue === $olderValue) {
|
||||
throw new ValueNotMergeableException($newerValue, $olderValue);
|
||||
}
|
||||
|
||||
if (null !== $currentValue && $newerValue === $currentValue) {
|
||||
throw new ValueNotMergeableException($newerValue, $olderValue);
|
||||
}
|
||||
|
||||
$isDefaultValue = null !== $defaultValue && $newerValue === $defaultValue;
|
||||
|
||||
if (self::isNotEmpty($newerValue) && !($newIsAnonymous && $isDefaultValue)) {
|
||||
return $newerValue;
|
||||
}
|
||||
|
||||
if (self::isNotEmpty($olderValue)) {
|
||||
return $olderValue;
|
||||
}
|
||||
|
||||
throw new ValueNotMergeableException($newerValue, $olderValue);
|
||||
}
|
||||
|
||||
public static function isNotEmpty($value): bool
|
||||
{
|
||||
return null !== $value && '' !== $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
|
||||
class MauticLeadExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\UniqueCustomField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Companies',
|
||||
operations: [
|
||||
new GetCollection(uriTemplate: '/companies', security: "is_granted('lead:leads:viewown')"),
|
||||
new Post(uriTemplate: '/companies', security: "is_granted('lead:leads:create')"),
|
||||
new Get(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:viewown')"),
|
||||
new Put(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:editown')"),
|
||||
new Patch(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:editother')"),
|
||||
new Delete(uriTemplate: '/companies/{id}', security: "is_granted('lead:leads:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['company:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['company:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Company extends FormEntity implements CustomFieldEntityInterface, IdentifierFieldEntityInterface
|
||||
{
|
||||
use CustomFieldEntityTrait;
|
||||
use ProjectTrait;
|
||||
|
||||
public const FIELD_ALIAS = 'company';
|
||||
public const TABLE_NAME = 'companies';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['company:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $score = 0;
|
||||
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private ?User $owner = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $socialCache = [];
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $address1;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $address2;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $phone;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $city;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $zipcode;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $country;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $website;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $industry;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
private $description;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getSocialCache()
|
||||
{
|
||||
return $this->socialCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $cache
|
||||
*/
|
||||
public function setSocialCache($cache): void
|
||||
{
|
||||
$this->socialCache = $cache;
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(CompanyRepository::class);
|
||||
|
||||
$builder->createField('id', 'integer')
|
||||
->makePrimaryKey()
|
||||
->generatedValue()
|
||||
->build();
|
||||
|
||||
$builder->createField('socialCache', 'array')
|
||||
->columnName('social_cache')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('owner', User::class)
|
||||
->cascadeMerge()
|
||||
->addJoinColumn('owner_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->createField('score', 'integer')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
self::loadFixedFieldMetadata(
|
||||
$builder,
|
||||
[
|
||||
'email',
|
||||
'address1',
|
||||
'address2',
|
||||
'phone',
|
||||
'city',
|
||||
'state',
|
||||
'zipcode',
|
||||
'country',
|
||||
'name',
|
||||
'website',
|
||||
'industry',
|
||||
'description',
|
||||
],
|
||||
FieldModel::$coreCompanyFields
|
||||
);
|
||||
|
||||
self::addProjectsField($builder, 'company_projects_xref', 'company_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('companyBasic')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'email',
|
||||
'address1',
|
||||
'address2',
|
||||
'phone',
|
||||
'city',
|
||||
'state',
|
||||
'zipcode',
|
||||
'country',
|
||||
'website',
|
||||
'industry',
|
||||
'description',
|
||||
'score',
|
||||
]
|
||||
)
|
||||
->setGroupPrefix('company')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'fields',
|
||||
'score',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'company');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addConstraint(new UniqueCustomField(['object' => 'company']));
|
||||
$metadata->addPropertyConstraint('score', new Assert\Range([
|
||||
'min' => 0,
|
||||
'max' => 2147483647,
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getDefaultIdentifierFields(): array
|
||||
{
|
||||
return [
|
||||
'companyname',
|
||||
'companyemail',
|
||||
'companywebsite',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
];
|
||||
}
|
||||
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
$prefix = 'company';
|
||||
|
||||
if (str_starts_with($prop, $prefix)) {
|
||||
$getter = 'get'.ucfirst(substr($prop, strlen($prefix)));
|
||||
$current = $this->$getter();
|
||||
if ($current !== $val) {
|
||||
$this->addChange($prop, [$current, $val]);
|
||||
}
|
||||
} elseif ('owner' === $prop) {
|
||||
$current = $this->getOwner();
|
||||
if ($current && !$val) {
|
||||
$this->changes['owner'] = [$current->getName().' ('.$current->getId().')', $val];
|
||||
} elseif (!$current && $val) {
|
||||
$this->changes['owner'] = [$current, $val->getName().' ('.$val->getId().')'];
|
||||
} elseif ($current && $current->getId() != $val->getId()) {
|
||||
$this->changes['owner'] = [
|
||||
$current->getName().'('.$current->getId().')',
|
||||
$val->getName().'('.$val->getId().')',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary identifier for the company.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrimaryIdentifier()
|
||||
{
|
||||
if ($name = $this->getName()) {
|
||||
return $name;
|
||||
} elseif (!empty($this->fields['core']['companyemail']['value'])) {
|
||||
return $this->fields['core']['companyemail']['value'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company
|
||||
*/
|
||||
public function setOwner(?User $owner = null)
|
||||
{
|
||||
$this->isChanged('owner', $owner);
|
||||
$this->owner = $owner;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOwner(): ?User
|
||||
{
|
||||
return $this->owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user to be used for permissions.
|
||||
*
|
||||
* @return User|int
|
||||
*/
|
||||
public function getPermissionUser()
|
||||
{
|
||||
return $this->getOwner() ?? $this->getCreatedBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $score
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setScore($score)
|
||||
{
|
||||
$score = (int) $score;
|
||||
|
||||
$this->isChanged('score', $score);
|
||||
$this->score = $score;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getScore()
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('companyname', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $email
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setEmail($email)
|
||||
{
|
||||
$this->isChanged('companyemail', $email);
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAddress1()
|
||||
{
|
||||
return $this->address1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $address1
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setAddress1($address1)
|
||||
{
|
||||
$this->isChanged('companyaddress1', $address1);
|
||||
$this->address1 = $address1;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAddress2()
|
||||
{
|
||||
return $this->address2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $address2
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setAddress2($address2)
|
||||
{
|
||||
$this->isChanged('companyaddress2', $address2);
|
||||
$this->address2 = $address2;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPhone()
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $phone
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setPhone($phone)
|
||||
{
|
||||
$this->isChanged('companyphone', $phone);
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCity()
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $city
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setCity($city)
|
||||
{
|
||||
$this->isChanged('companycity', $city);
|
||||
$this->city = $city;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $state
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setState($state)
|
||||
{
|
||||
$this->isChanged('companystate', $state);
|
||||
$this->state = $state;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getZipcode()
|
||||
{
|
||||
return $this->zipcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $zipcode
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setZipcode($zipcode)
|
||||
{
|
||||
$this->isChanged('companyzipcode', $zipcode);
|
||||
$this->zipcode = $zipcode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCountry()
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $country
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setCountry($country)
|
||||
{
|
||||
$this->isChanged('companycountry', $country);
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getWebsite()
|
||||
{
|
||||
return $this->website;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $website
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setWebsite($website)
|
||||
{
|
||||
$this->isChanged('companywebsite', $website);
|
||||
$this->website = $website;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getIndustry()
|
||||
{
|
||||
return $this->industry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $industry
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setIndustry($industry)
|
||||
{
|
||||
$this->isChanged('companyindustry', $industry);
|
||||
$this->industry = $industry;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $description
|
||||
*
|
||||
* @return Company
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('companydescription', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class CompanyChangeLog
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $eventName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $actionName;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_companies_change_log')
|
||||
->setCustomRepositoryClass(CompanyChangeLogRepository::class)
|
||||
->addIndex(['date_added'], 'company_date_added');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'companyChangeLog');
|
||||
|
||||
$builder->createField('type', 'text')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createField('eventName', 'string')
|
||||
->columnName('event_name')
|
||||
->build();
|
||||
|
||||
$builder->createField('actionName', 'string')
|
||||
->columnName('action_name')
|
||||
->build();
|
||||
|
||||
$builder->createField('company', 'integer')
|
||||
->columnName('company_id')
|
||||
->build();
|
||||
|
||||
$builder->addDateAdded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set eventName.
|
||||
*
|
||||
* @param string $eventName
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setEventName($eventName)
|
||||
{
|
||||
$this->eventName = $eventName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get eventName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEventName()
|
||||
{
|
||||
return $this->eventName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set actionName.
|
||||
*
|
||||
* @param string $actionName
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setActionName($actionName)
|
||||
{
|
||||
$this->actionName = $actionName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actionName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getActionName()
|
||||
{
|
||||
return $this->actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set delta.
|
||||
*
|
||||
* @param int $company
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setCompany($company)
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCompany()
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateAdded.
|
||||
*
|
||||
* @param \DateTime $dateAdded
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateAdded.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lead.
|
||||
*
|
||||
* @return CompanyChangeLog
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lead.
|
||||
*
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<CompanyChangeLog>
|
||||
*/
|
||||
class CompanyChangeLogRepository extends CommonRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class CompanyLead
|
||||
{
|
||||
/**
|
||||
* @var Company
|
||||
**/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $primary = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('companies_leads')
|
||||
->setCustomRepositoryClass(CompanyLeadRepository::class);
|
||||
|
||||
$builder->createManyToOne('company', 'Company')
|
||||
->makePrimaryKey()
|
||||
->addJoinColumn('company_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', true);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('primary', 'boolean')
|
||||
->columnName('is_primary')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
*/
|
||||
public function setDateAdded($date): void
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company
|
||||
*/
|
||||
public function getCompany()
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company
|
||||
*/
|
||||
public function getCompanies()
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Company $company
|
||||
*/
|
||||
public function setCompany($company): void
|
||||
{
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $primary
|
||||
*/
|
||||
public function setPrimary($primary): void
|
||||
{
|
||||
$this->primary = $primary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getPrimary()
|
||||
{
|
||||
return $this->primary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Exception\PrimaryCompanyNotFoundException;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<CompanyLead>
|
||||
*/
|
||||
class CompanyLeadRepository extends CommonRepository
|
||||
{
|
||||
public const DELETE_BATCH_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* @param CompanyLead[] $entities
|
||||
*/
|
||||
public function saveEntities($entities, $new = true): void
|
||||
{
|
||||
// Get a list of contacts and set primary to 0
|
||||
if ($new) {
|
||||
$contacts = [];
|
||||
$contactId = null;
|
||||
foreach ($entities as $entity) {
|
||||
$contactId = $entity->getLead()->getId();
|
||||
if (!isset($contacts[$contactId])) {
|
||||
// Set one company from the batch as as primary
|
||||
$entity->setPrimary(true);
|
||||
}
|
||||
|
||||
$contacts[$contactId] = $contactId;
|
||||
}
|
||||
|
||||
if ($contactId) {
|
||||
// Only one company should be set as primary so reset all in order to let the entity update the one
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'companies_leads')
|
||||
->set('is_primary', 0);
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->in('lead_id', $contacts)
|
||||
)->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
parent::saveEntities($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get companies by leadId.
|
||||
*/
|
||||
public function getCompaniesByLeadId($leadId, $companyId = null, ?bool $onlyPrimary = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('cl.company_id, cl.date_added as date_associated, cl.is_primary, comp.*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->join('cl', MAUTIC_TABLE_PREFIX.'companies', 'comp', 'comp.id = cl.company_id')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
|
||||
if ($companyId) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('cl.company_id', ':companyId')
|
||||
)->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
if ($onlyPrimary) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('cl.is_primary', true)
|
||||
);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws PrimaryCompanyNotFoundException
|
||||
*/
|
||||
public function getPrimaryCompanyByLeadId(int $leadId): array
|
||||
{
|
||||
$companies = $this->getCompaniesByLeadId($leadId);
|
||||
foreach ($companies as $company) {
|
||||
if ($company['is_primary']) {
|
||||
return $company;
|
||||
}
|
||||
}
|
||||
|
||||
throw new PrimaryCompanyNotFoundException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getCompanyIdsByLeadId(string $leadId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('cl.company_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
|
||||
return array_map(
|
||||
fn (array $company) => (string) $company['company_id'],
|
||||
$q->executeQuery()->fetchAllAssociative()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $companyId
|
||||
*/
|
||||
public function getCompanyLeads($companyId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('cl.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl');
|
||||
|
||||
$q->where($q->expr()->eq('cl.company_id', ':company'))
|
||||
->setParameter('company', $companyId);
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getLatestCompanyForLead($leadId)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('cl.company_id, comp.companyname, comp.companycity, comp.companycountry')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->join('cl', MAUTIC_TABLE_PREFIX.'companies', 'comp', 'comp.id = cl.company_id')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
$q->orderBy('cl.date_added', 'DESC');
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return !empty($result) ? $result[0] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getCompanyLeadEntity($leadId, $companyId): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('cl.is_primary, cl.lead_id, cl.company_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->where(
|
||||
$qb->expr()->eq('cl.lead_id', ':leadId'),
|
||||
$qb->expr()->eq('cl.company_id', ':companyId')
|
||||
)->setParameter('leadId', $leadId)
|
||||
->setParameter('companyId', $companyId);
|
||||
|
||||
return $qb->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntitiesByLead(Lead $lead)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('cl')
|
||||
->from(CompanyLead::class, 'cl')
|
||||
->where(
|
||||
$qb->expr()->eq('cl.lead', ':lead')
|
||||
)->setParameter('lead', $lead);
|
||||
|
||||
return $qb->getQuery()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates leads company name If company name changed and company is primary.
|
||||
*/
|
||||
public function updateLeadsPrimaryCompanyName(Company $company): void
|
||||
{
|
||||
if ($company->isNew() || empty($company->getChanges()['fields']['companyname'])) {
|
||||
return;
|
||||
}
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('cl.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl');
|
||||
$q->where($q->expr()->eq('cl.company_id', ':companyId'))
|
||||
->setParameter('companyId', $company->getId())
|
||||
->andWhere('cl.is_primary = 1');
|
||||
$leadIds = $q->executeQuery()->fetchOne();
|
||||
if (!empty($leadIds)) {
|
||||
$this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'leads')
|
||||
->set('company', ':company')
|
||||
->setParameter('company', $company->getName())
|
||||
->where(
|
||||
$q->expr()->in('id', $leadIds)
|
||||
)->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeContactPrimaryCompany(int $leadId): void
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'companies_leads');
|
||||
$qb->where(
|
||||
$qb->expr()->eq('lead_id', $leadId)
|
||||
)->andWhere(
|
||||
$qb->expr()->eq('is_primary', 1)
|
||||
)->executeStatement();
|
||||
}
|
||||
|
||||
public function removeAllSecondaryCompanies(): void
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
do {
|
||||
$sql = 'DELETE FROM '.MAUTIC_TABLE_PREFIX.'companies_leads WHERE is_primary = 0 LIMIT '.self::DELETE_BATCH_SIZE;
|
||||
$row = $conn->executeStatement($sql);
|
||||
} while ($row);
|
||||
}
|
||||
|
||||
public function removeContactSecondaryCompanies(int $leadId): void
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'companies_leads');
|
||||
$qb->where(
|
||||
$qb->expr()->eq('lead_id', $leadId)
|
||||
)->andWhere(
|
||||
$qb->expr()->eq('is_primary', 0)
|
||||
)->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Event\CompanyBuildSearchEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Company>
|
||||
*/
|
||||
class CompanyRepository extends CommonRepository implements CustomFieldRepositoryInterface
|
||||
{
|
||||
use CustomFieldRepositoryTrait;
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $availableSearchFields = [];
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface|null
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
/**
|
||||
* Used by search functions to search using aliases as commands.
|
||||
*/
|
||||
public function setAvailableSearchFields(array $fields): void
|
||||
{
|
||||
$this->availableSearchFields = $fields;
|
||||
}
|
||||
|
||||
public function setDispatcher(EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*/
|
||||
public function getEntity($id = 0): ?Company
|
||||
{
|
||||
try {
|
||||
$q = $this->createQueryBuilder($this->getTableAlias());
|
||||
if (is_array($id)) {
|
||||
$this->buildSelectClause($q, $id);
|
||||
$companyId = (int) $id['id'];
|
||||
} else {
|
||||
$companyId = $id;
|
||||
}
|
||||
$q->andWhere($this->getTableAlias().'.id = '.(int) $companyId);
|
||||
$entity = $q->getQuery()->getSingleResult();
|
||||
} catch (\Exception) {
|
||||
$entity = null;
|
||||
}
|
||||
|
||||
if (null === $entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($entity->getFields()) {
|
||||
// Pulled from Doctrine memory so don't make unnecessary queries as this has already happened
|
||||
return $entity;
|
||||
}
|
||||
|
||||
$fieldValues = $this->getFieldValues($id, true, 'company');
|
||||
$entity->setFields($fieldValues);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of leads.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
return $this->getEntitiesWithCustomFields('company', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Query\QueryBuilder
|
||||
*/
|
||||
public function getEntitiesDbalQueryBuilder()
|
||||
{
|
||||
return $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', $this->getTableAlias());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getEntitiesOrmQueryBuilder($order, array $args=[])
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
$q->select($this->getTableAlias().','.$order)
|
||||
->from(Company::class, $this->getTableAlias(), $this->getTableAlias().'.id');
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the groups available for fields.
|
||||
*/
|
||||
public function getFieldGroups(): array
|
||||
{
|
||||
return ['core', 'professional', 'other'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get companies by lead.
|
||||
*/
|
||||
public function getCompaniesByLeadId($leadId, $companyId = null): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('comp.*, cl.is_primary')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'comp')
|
||||
->leftJoin('comp', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'cl.company_id = comp.id')
|
||||
->where('cl.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId)
|
||||
->orderBy('cl.is_primary', 'DESC');
|
||||
|
||||
if ($companyId) {
|
||||
$q->andWhere('comp.id = :companyId')->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'comp';
|
||||
}
|
||||
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
$customFields = $this->getSearchableFieldAliases($this->getEntityManager()->getRepository(LeadField::class), 'company');
|
||||
$availableForSearch = array_map(fn ($alias) => 'comp.'.$alias, $customFields);
|
||||
|
||||
$columns = array_merge(
|
||||
[
|
||||
'comp.companyname',
|
||||
'comp.companyemail',
|
||||
],
|
||||
$availableForSearch,
|
||||
);
|
||||
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
$columns
|
||||
);
|
||||
}
|
||||
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = true;
|
||||
$command = $filter->command;
|
||||
|
||||
if (in_array($command, [
|
||||
$this->translator->trans('mautic.project.searchcommand.name'),
|
||||
$this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'),
|
||||
])) {
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'company_id',
|
||||
'company_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if (in_array($command, $this->availableSearchFields)) {
|
||||
$expr = $q->expr()->like($this->getTableAlias().".$command", ":$unique");
|
||||
}
|
||||
|
||||
if ($this->dispatcher) {
|
||||
$event = new CompanyBuildSearchEvent($filter->string, $filter->command, $unique, $filter->not, $q);
|
||||
$this->dispatcher->dispatch($event, LeadEvents::COMPANY_BUILD_SEARCH_COMMANDS);
|
||||
if ($event->isSearchDone()) {
|
||||
$returnParameter = $event->getReturnParameters();
|
||||
$filter->strict = $event->getStrict();
|
||||
$expr = $event->getSubQuery();
|
||||
$parameters = array_merge($parameters, $event->getParameters());
|
||||
}
|
||||
}
|
||||
|
||||
if ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters[$unique] = $string;
|
||||
}
|
||||
|
||||
return [
|
||||
$expr,
|
||||
$parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = array_merge(['mautic.project.searchcommand.name'], $this->getStandardSearchCommands());
|
||||
if (!empty($this->availableSearchFields)) {
|
||||
$commands = array_merge($commands, $this->availableSearchFields);
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $user
|
||||
* @param string $id
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getCompanies($user = false, $id = '')
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
static $companies = [];
|
||||
|
||||
if ($user) {
|
||||
$user = $this->currentUser;
|
||||
}
|
||||
|
||||
$key = (int) $id;
|
||||
if (isset($companies[$key])) {
|
||||
return $companies[$key];
|
||||
}
|
||||
|
||||
$q->select('comp.*, cl.is_primary')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'comp')
|
||||
->leftJoin('comp', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'cl.company_id = comp.id');
|
||||
|
||||
if (!empty($id)) {
|
||||
$q->where(
|
||||
$q->expr()->eq('comp.id', $id)
|
||||
);
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$q->andWhere('comp.created_by = :user');
|
||||
$q->setParameter('user', $user->getId());
|
||||
}
|
||||
|
||||
$q->orderBy('comp.companyname', 'ASC');
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$companies[$key] = $results;
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of leads that belong to the company.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadCount($companyIds)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(cl.lead_id) as thecount, cl.company_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl');
|
||||
|
||||
$returnArray = is_array($companyIds);
|
||||
|
||||
if (!$returnArray) {
|
||||
$companyIds = [$companyIds];
|
||||
}
|
||||
|
||||
$q->where(
|
||||
$q->expr()->in('cl.company_id', $companyIds)
|
||||
)
|
||||
->groupBy('cl.company_id');
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
foreach ($result as $r) {
|
||||
$return[$r['company_id']] = $r['thecount'];
|
||||
}
|
||||
|
||||
// Ensure lists without leads have a value
|
||||
foreach ($companyIds as $l) {
|
||||
if (!isset($return[$l])) {
|
||||
$return[$l] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return ($returnArray) ? $return : $return[$companyIds[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of lists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function identifyCompany($companyName, $city = null, $country = null, $state = null)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
if (empty($companyName)) {
|
||||
return [];
|
||||
}
|
||||
$q->select('comp.id, comp.companyname, comp.companycity, comp.companycountry, comp.companystate')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'comp');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->eq('comp.companyname', ':companyName')
|
||||
)->setParameter('companyName', $companyName);
|
||||
|
||||
if ($city) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('comp.companycity', ':city')
|
||||
)->setParameter('city', $city);
|
||||
}
|
||||
if ($country) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('comp.companycountry', ':country')
|
||||
)->setParameter('country', $country);
|
||||
}
|
||||
if ($state) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('comp.companystate', ':state')
|
||||
)->setParameter('state', $state);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return ($results) ? $results[0] : null;
|
||||
}
|
||||
|
||||
public function getCompaniesForContacts(array $contacts): array
|
||||
{
|
||||
if (!$contacts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('c.*, l.lead_id, l.is_primary')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'c')
|
||||
->join('c', MAUTIC_TABLE_PREFIX.'companies_leads', 'l', 'l.company_id = c.id')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->in('l.lead_id', $contacts)
|
||||
)
|
||||
)
|
||||
->orderBy('l.date_added, l.company_id', 'DESC'); // primary should be [0]
|
||||
|
||||
$companies = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
// Group companies per contact
|
||||
$contactCompanies = [];
|
||||
foreach ($companies as $company) {
|
||||
if (!isset($contactCompanies[$company['lead_id']])) {
|
||||
$contactCompanies[$company['lead_id']] = [];
|
||||
}
|
||||
|
||||
$contactCompanies[$company['lead_id']][] = $company;
|
||||
}
|
||||
|
||||
return $contactCompanies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get companies grouped by column.
|
||||
*
|
||||
* @param \Doctrine\DBAL\Query\QueryBuilder $query
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getCompaniesByGroup($query, $column): array
|
||||
{
|
||||
$query->select('count(comp.id) as companies, '.$column)
|
||||
->addGroupBy($column)
|
||||
->andWhere(
|
||||
$query->expr()->and(
|
||||
$query->expr()->isNotNull($column),
|
||||
$query->expr()->neq($column, $query->expr()->literal(''))
|
||||
)
|
||||
);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMostCompanies($query, $limit = 10, $offset = 0)
|
||||
{
|
||||
$query->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $valueColumn
|
||||
*/
|
||||
public function getAjaxSimpleList(
|
||||
?CompositeExpression $expr = null,
|
||||
array $parameters = [],
|
||||
$labelColumn = null,
|
||||
$valueColumn = 'id',
|
||||
int $limit = 10,
|
||||
int $start = 0,
|
||||
): array {
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$alias = $prefix = $this->getTableAlias();
|
||||
if (!empty($prefix)) {
|
||||
$prefix .= '.';
|
||||
}
|
||||
|
||||
$tableName = $this->_em->getClassMetadata($this->getEntityName())->getTableName();
|
||||
|
||||
$class = '\\'.$this->getClassName();
|
||||
$reflection = new \ReflectionClass(new $class());
|
||||
|
||||
// Get the label column if necessary
|
||||
if (null == $labelColumn) {
|
||||
if ($reflection->hasMethod('getTitle')) {
|
||||
$labelColumn = 'title';
|
||||
} else {
|
||||
$labelColumn = 'name';
|
||||
}
|
||||
}
|
||||
|
||||
if (!(isset($parameters['onlyNames']) && $parameters['onlyNames'])) {
|
||||
$labelExpression = '
|
||||
case
|
||||
when (comp.companycountry is not null and comp.companycity is not null) then concat(comp.companyname, \' <small>\', companycity,\', \', companycountry, \'</small>\')
|
||||
when (comp.companycountry is not null) then concat(comp.companyname, \' <small>\', comp.companycountry, \'</small>\')
|
||||
when (comp.companycity is not null) then concat(comp.companyname, \' <small>\', comp.companycity, \'</small>\')
|
||||
else comp.companyname
|
||||
end
|
||||
as label';
|
||||
} else {
|
||||
$labelExpression = $prefix.' companyname as label';
|
||||
}
|
||||
|
||||
$q->select($prefix.$valueColumn.' as value, '.$labelExpression)
|
||||
->from($tableName, $alias)
|
||||
->orderBy($prefix.$labelColumn);
|
||||
|
||||
if (null !== $expr && $expr->count()) {
|
||||
$q->where($expr);
|
||||
}
|
||||
|
||||
if (!empty($parameters)) {
|
||||
$q->setParameters($parameters);
|
||||
}
|
||||
|
||||
// Published only
|
||||
if ($reflection->hasMethod('getIsPublished')) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq($prefix.'is_published', ':true')
|
||||
)
|
||||
->setParameter('true', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of company Ids by unique field data.
|
||||
*
|
||||
* @param iterable<mixed> $uniqueFieldsWithData An array of columns & values to filter by
|
||||
* @param int|null $companyId The current company id. Added to query to skip and find other companies
|
||||
* @param int|null $limit Limit count of results to return
|
||||
*
|
||||
* @return array<array{id: string}>
|
||||
*/
|
||||
public function getCompanyIdsByUniqueFields($uniqueFieldsWithData, ?int $companyId = null, ?int $limit = null): array
|
||||
{
|
||||
return $this->getCompanyFieldsByUniqueFields($uniqueFieldsWithData, 'c.id', $companyId, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of company Ids by unique field data.
|
||||
*
|
||||
* @param iterable<mixed> $uniqueFieldsWithData An array of columns & values to filter by
|
||||
* @param int|null $companyId The current company id. Added to query to skip and find other companies
|
||||
* @param int|null $limit Limit count of results to return
|
||||
*
|
||||
* @return array<array{id: string}>
|
||||
*/
|
||||
public function getCompanyFieldsByUniqueFields($uniqueFieldsWithData, string $select, ?int $companyId = null, ?int $limit = null): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select($select)
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies', 'c');
|
||||
|
||||
// loop through the fields and
|
||||
foreach ($uniqueFieldsWithData as $col => $val) {
|
||||
$q->{$this->getUniqueIdentifiersWherePart()}("c.$col = :".$col)
|
||||
->setParameter($col, $val);
|
||||
}
|
||||
|
||||
// if we have a company ID lets use it
|
||||
if ($companyId > 0) {
|
||||
// make sure that it's not the id we already have
|
||||
$q->andWhere('c.id != :companyId')
|
||||
->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$q->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Company[]
|
||||
*/
|
||||
public function getCompaniesByUniqueFields(array $uniqueFieldsWithData, ?int $companyId = null, ?int $limit = null): array
|
||||
{
|
||||
$results = $this->getCompanyFieldsByUniqueFields($uniqueFieldsWithData, 'c.*', $companyId, $limit);
|
||||
|
||||
// Collect the IDs
|
||||
$companies = [];
|
||||
foreach ($results as $r) {
|
||||
$companies[(int) $r['id']] = $r;
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->select('c')
|
||||
->from(Company::class, 'c');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->in('c.id', ':ids')
|
||||
)
|
||||
->setParameter('ids', array_keys($companies))
|
||||
->orderBy('c.dateAdded', Order::Descending->value)
|
||||
->addOrderBy('c.id', Order::Descending->value);
|
||||
|
||||
$entities = $q->getQuery()
|
||||
->getResult();
|
||||
|
||||
/** @var Company $company */
|
||||
foreach ($entities as $company) {
|
||||
$company->setFields(
|
||||
$this->formatFieldValues($companies[$company->getId()], true, 'company')
|
||||
);
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string[]>
|
||||
*/
|
||||
public function getCompanyLookupData(string $filterVal): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('id, companyname, companycity, companystate')
|
||||
->from(MAUTIC_TABLE_PREFIX.Company::TABLE_NAME)
|
||||
->where($q->expr()->eq('is_published', true))
|
||||
->andWhere($q->expr()->like('companyname', ':filterVar'))
|
||||
->setParameter('filterVar', '%'.$filterVal.'%')
|
||||
->orderBy('companyname')
|
||||
->setMaxResults(50);
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata as ValidatorClassMetadata;
|
||||
|
||||
class ContactExportScheduler
|
||||
{
|
||||
private ?int $id = null;
|
||||
|
||||
private ?User $user = null; // Created by
|
||||
|
||||
private \DateTimeImmutable $scheduledDateTime;
|
||||
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $data = [];
|
||||
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $changes = [];
|
||||
|
||||
/**
|
||||
* @template T of ClassMetadata
|
||||
*
|
||||
* @param T $metadata
|
||||
*/
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('contact_export_scheduler');
|
||||
$builder->setCustomRepositoryClass(ContactExportSchedulerRepository::class);
|
||||
$builder->addId();
|
||||
$builder->createManyToOne('user', User::class)
|
||||
->addJoinColumn('user_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
$builder->createField('scheduledDateTime', Types::DATETIME_IMMUTABLE)
|
||||
->columnName('scheduled_datetime')
|
||||
->build();
|
||||
$builder->addNullableField('data', Types::ARRAY);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ValidatorClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint(
|
||||
'scheduledDate',
|
||||
new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.import.dir.notblank']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->addChange('user', $user->getId());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getScheduledDateTime(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->scheduledDateTime;
|
||||
}
|
||||
|
||||
public function setScheduledDateTime(\DateTimeImmutable $scheduledDateTime): self
|
||||
{
|
||||
$this->scheduledDateTime = $scheduledDateTime;
|
||||
$this->addChange('scheduledDateTime', $scheduledDateTime);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*/
|
||||
public function setData(array $data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->addChange('data', $data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getChanges(): array
|
||||
{
|
||||
return $this->changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function addChange(string $property, $value): void
|
||||
{
|
||||
$this->changes[$property] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<ContactExportScheduler>
|
||||
*/
|
||||
class ContactExportSchedulerRepository extends CommonRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
interface CustomFieldEntityInterface
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $fields
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setFields($fields);
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFields();
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
* @param mixed $value
|
||||
* @param string $oldValue
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function addUpdatedField($alias, $value, $oldValue = '');
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUpdatedFields();
|
||||
|
||||
/**
|
||||
* Get a field value (should include those in updated fields).
|
||||
*
|
||||
* @param string $field alias
|
||||
* @param string|null $group
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFieldValue($field, $group = null);
|
||||
|
||||
/**
|
||||
* Get field details.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $group
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getField($key, $group = null);
|
||||
|
||||
/**
|
||||
* Get flat array of profile fields without groups.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProfileFields();
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\LeadBundle\Field\SchemaDefinition;
|
||||
use Mautic\LeadBundle\Helper\CustomFieldHelper;
|
||||
use Mautic\LeadBundle\Helper\CustomFieldValueHelper;
|
||||
|
||||
trait CustomFieldEntityTrait
|
||||
{
|
||||
/**
|
||||
* Used by Mautic to populate the fields pulled from the DB.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* Just a place to store updated field values so we don't have to loop through them again comparing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $updatedFields = [];
|
||||
|
||||
/**
|
||||
* A place events can use to pass data around on the object to prevent issues like creating a contact and having it processed to be sent back
|
||||
* to the origin of creation in a webhook.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $eventData = [];
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->getFieldValue(strtolower($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
return $this->addUpdatedField(strtolower($name), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
$isSetter = str_starts_with($name, 'set');
|
||||
$isGetter = str_starts_with($name, 'get');
|
||||
|
||||
if (($isSetter && array_key_exists(0, $arguments)) || $isGetter) {
|
||||
$fieldRequested = mb_strtolower(mb_substr($name, 3));
|
||||
$fields = $this->getProfileFields();
|
||||
|
||||
if (array_key_exists($fieldRequested, $fields)) {
|
||||
return ($isSetter) ? $this->addUpdatedField($fieldRequested, $arguments[0]) : $this->getFieldValue($fieldRequested);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::__call($name, $arguments);
|
||||
}
|
||||
|
||||
public function setFields($fields): void
|
||||
{
|
||||
$this->fields = CustomFieldValueHelper::normalizeValues($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $ungroup
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFields($ungroup = false)
|
||||
{
|
||||
if ($ungroup && isset($this->fields['core'])) {
|
||||
$return = [];
|
||||
foreach ($this->fields as $fields) {
|
||||
$return += $fields;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an updated field to persist to the DB and to note changes.
|
||||
*
|
||||
* @param string $oldValue
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addUpdatedField($alias, $value, $oldValue = null)
|
||||
{
|
||||
// Don't allow overriding ID
|
||||
if ('id' === $alias) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$property = (defined('self::FIELD_ALIAS')) ? str_replace(self::FIELD_ALIAS, '', $alias) : $alias;
|
||||
$field = $this->getField($alias);
|
||||
$setter = 'set'.ucfirst($property);
|
||||
|
||||
if (null == $oldValue) {
|
||||
$oldValue = $this->getFieldValue($alias);
|
||||
} elseif ($field) {
|
||||
$oldValue = CustomFieldHelper::fixValueType($field['type'], $oldValue);
|
||||
}
|
||||
|
||||
if (property_exists($this, $property) && method_exists($this, $setter)) {
|
||||
// Fixed custom field so use the setter but don't get caught in a loop such as a custom field called "notes"
|
||||
// Set empty value as null
|
||||
if ('' === $value) {
|
||||
$value = null;
|
||||
}
|
||||
$this->$setter($value);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
if ('' === $value) {
|
||||
// Ensure value is null for consistency
|
||||
$value = null;
|
||||
|
||||
if ('' === $oldValue) {
|
||||
$oldValue = null;
|
||||
}
|
||||
}
|
||||
} elseif (is_array($value)) {
|
||||
// Flatten the array
|
||||
$value = implode('|', $value);
|
||||
}
|
||||
|
||||
if ($field) {
|
||||
$value = CustomFieldHelper::fixValueType($field['type'], $value);
|
||||
}
|
||||
|
||||
if ($oldValue !== $value && !(('' === $oldValue && null === $value) || (null === $oldValue && '' === $value))) {
|
||||
$this->addChange('fields', [$alias => [$oldValue, $value]]);
|
||||
$this->updatedFields[$alias] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of updated fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUpdatedFields()
|
||||
{
|
||||
return $this->updatedFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string|null $group
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFieldValue($field, $group = null)
|
||||
{
|
||||
if (property_exists($this, $field)) {
|
||||
$value = $this->{'get'.ucfirst($field)}();
|
||||
|
||||
if (null !== $value) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($field, $this->updatedFields)) {
|
||||
return $this->updatedFields[$field];
|
||||
}
|
||||
|
||||
if ($field = $this->getField($field, $group)) {
|
||||
return CustomFieldHelper::fixValueType($field['type'], $field['value']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field details.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $group
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getField($key, $group = null)
|
||||
{
|
||||
if ($group && isset($this->fields[$group][$key])) {
|
||||
return $this->fields[$group][$key];
|
||||
}
|
||||
|
||||
foreach ($this->fields as $groupFields) {
|
||||
foreach ($groupFields as $name => $details) {
|
||||
if ($name == $key) {
|
||||
return $details;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getProfileFields()
|
||||
{
|
||||
if (isset($this->fields['core'])) {
|
||||
$fieldValues = [
|
||||
'id' => $this->id,
|
||||
];
|
||||
|
||||
foreach ($this->fields as $group => $fields) {
|
||||
if ('all' === $group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fields as $alias => $field) {
|
||||
$fieldValues[$alias] = $field['value'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($fieldValues, $this->updatedFields);
|
||||
} else {
|
||||
// The fields are already flattened
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
}
|
||||
|
||||
public function hasFields(): bool
|
||||
{
|
||||
return !empty($this->fields);
|
||||
}
|
||||
|
||||
public function getEventData($key)
|
||||
{
|
||||
return $this->eventData[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setEventData($key, $value)
|
||||
{
|
||||
$this->eventData[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected static function loadFixedFieldMetadata(ClassMetadataBuilder $builder, array $fields, array $customFieldDefinitions)
|
||||
{
|
||||
foreach ($fields as $fieldProperty) {
|
||||
$field = (defined('self::FIELD_ALIAS')) ? self::FIELD_ALIAS.$fieldProperty : $fieldProperty;
|
||||
|
||||
$type = 'text';
|
||||
if (isset($customFieldDefinitions[$field]) && !empty($customFieldDefinitions[$field]['type'])) {
|
||||
$type = $customFieldDefinitions[$field]['type'];
|
||||
}
|
||||
|
||||
$builder->addNamedField(
|
||||
$fieldProperty,
|
||||
SchemaDefinition::getSchemaDefinition($field, $type, !empty($customFieldDefinitions[$field]['unique']))['type'],
|
||||
$field,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder;
|
||||
use Doctrine\ORM\QueryBuilder as OrmQueryBuilder;
|
||||
|
||||
/**
|
||||
* Interface CustomFieldRepositoryInterface.
|
||||
*/
|
||||
interface CustomFieldRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Return an array of groups supported by the custom fields for this entity.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFieldGroups();
|
||||
|
||||
/**
|
||||
* Get the base DBAL query builder for entities.
|
||||
*
|
||||
* @return DbalQueryBuilder
|
||||
*/
|
||||
public function getEntitiesDbalQueryBuilder();
|
||||
|
||||
/**
|
||||
* Get the base DBAL query builder for entities.
|
||||
*
|
||||
* @return OrmQueryBuilder
|
||||
*/
|
||||
public function getEntitiesOrmQueryBuilder($order);
|
||||
|
||||
/**
|
||||
* Requires table alias.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getTableAlias();
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheHelper;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
||||
use Mautic\LeadBundle\Controller\ListController;
|
||||
use Mautic\LeadBundle\Helper\CustomFieldHelper;
|
||||
|
||||
trait CustomFieldRepositoryTrait
|
||||
{
|
||||
protected $useDistinctCount = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $customFieldList = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $uniqueIdentifiersOperator;
|
||||
|
||||
/**
|
||||
* @param string $object
|
||||
* @param array $args
|
||||
*/
|
||||
public function getEntitiesWithCustomFields($object, $args, $resultsCallback = null)
|
||||
{
|
||||
$skipOrdering = $args['skipOrdering'] ?? false;
|
||||
[$fields, $fixedFields] = $this->getCustomFieldList($object);
|
||||
|
||||
// Fix arguments if necessary
|
||||
$args = $this->convertOrmProperties($this->getClassName(), $args);
|
||||
|
||||
// DBAL
|
||||
/** @var QueryBuilder $dq */
|
||||
$dq = $args['qb'] ?? $this->getEntitiesDbalQueryBuilder();
|
||||
|
||||
// Generate where clause first to know if we need to use distinct on primary ID or not
|
||||
$this->useDistinctCount = false;
|
||||
$this->buildWhereClause($dq, $args);
|
||||
|
||||
if (!empty($args['withTotalCount']) || !isset($args['count'])) {
|
||||
// Distinct is required here to get the correct count when group by is used due to applied filters
|
||||
$countSelect = ($this->useDistinctCount) ? 'COUNT(DISTINCT('.$this->getTableAlias().'.id))' : 'COUNT('.$this->getTableAlias().'.id)';
|
||||
$dq->select($countSelect.' as count');
|
||||
|
||||
// Advanced search filters may have set a group by and if so, let's remove it for the count.
|
||||
if ($groupBy = $dq->getQueryPart('groupBy')) {
|
||||
$dq->resetQueryPart('groupBy');
|
||||
}
|
||||
|
||||
// get a total count
|
||||
if (!empty($args['totalCountTtl'])) {
|
||||
$statement = ResultCacheHelper::executeCachedDbalQuery($this->getEntityManager()->getConnection(), $dq, new ResultCacheOptions($object.'-total-count', $args['totalCountTtl']));
|
||||
} else {
|
||||
$statement = $dq->executeQuery();
|
||||
}
|
||||
|
||||
$result = $statement->fetchAllAssociative();
|
||||
$total = ($result) ? $result[0]['count'] : 0;
|
||||
} else {
|
||||
$total = $args['count'];
|
||||
}
|
||||
|
||||
if (!$total && !empty($args['withTotalCount'])) {
|
||||
$results = [];
|
||||
} else {
|
||||
if (isset($groupBy) && $groupBy) {
|
||||
$dq->groupBy($groupBy);
|
||||
}
|
||||
// now get the actual paginated results
|
||||
|
||||
$this->buildOrderByClause($dq, $args);
|
||||
$this->buildLimiterClauses($dq, $args);
|
||||
|
||||
$dq->resetQueryPart('select');
|
||||
$this->buildSelectClause($dq, $args);
|
||||
|
||||
$results = $dq->executeQuery()->fetchAllAssociative();
|
||||
if (isset($args['route']) && ListController::ROUTE_SEGMENT_CONTACTS == $args['route']) {
|
||||
unset($args['select']); // Our purpose of getting list of ids has already accomplished. We no longer need this.
|
||||
}
|
||||
|
||||
// loop over results to put fields in something that can be assigned to the entities
|
||||
$fieldValues = [];
|
||||
$groups = $this->getFieldGroups();
|
||||
|
||||
foreach ($results as $result) {
|
||||
$id = $result['id'];
|
||||
// unset all the columns that are not fields
|
||||
$this->removeNonFieldColumns($result, $fixedFields);
|
||||
|
||||
foreach ($result as $k => $r) {
|
||||
if (isset($fields[$k])) {
|
||||
$fieldValues[$id][$fields[$k]['group']][$fields[$k]['alias']] = $fields[$k];
|
||||
$fieldValues[$id][$fields[$k]['group']][$fields[$k]['alias']]['value'] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure each group key is present
|
||||
foreach ($groups as $g) {
|
||||
if (!isset($fieldValues[$id][$g])) {
|
||||
$fieldValues[$id][$g] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unset($results, $fields);
|
||||
|
||||
// get an array of IDs for ORM query
|
||||
$ids = array_keys($fieldValues);
|
||||
|
||||
if (count($ids)) {
|
||||
if ($skipOrdering) {
|
||||
$alias = $this->getTableAlias();
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
$q->select($alias)
|
||||
->from(Lead::class, $alias, $alias.'.id')
|
||||
->indexBy($alias, $alias.'.id');
|
||||
} else {
|
||||
// ORM
|
||||
|
||||
// build the order by id since the order was applied above
|
||||
// unfortunately, doctrine does not have a way to natively support this and can't use MySQL's FIELD function
|
||||
// since we have to be cross-platform; it's way ugly
|
||||
|
||||
// We should probably totally ditch orm for leads
|
||||
|
||||
// This "hack" is in place to allow for custom ordering in the API.
|
||||
// See https://github.com/mautic/mautic/pull/7494#issuecomment-600970208
|
||||
$order = '(CASE';
|
||||
foreach ($ids as $count => $id) {
|
||||
$order .= ' WHEN '.$this->getTableAlias().'.id = '.$id.' THEN '.$count;
|
||||
++$count;
|
||||
}
|
||||
$order .= ' ELSE '.$count.' END) AS HIDDEN ORD';
|
||||
|
||||
// ORM - generates lead entities
|
||||
/** @var \Doctrine\ORM\QueryBuilder $q */
|
||||
$q = $this->getEntitiesOrmQueryBuilder($order, $args);
|
||||
$this->buildSelectClause($dq, $args);
|
||||
|
||||
$q->orderBy('ORD', Order::Ascending->value);
|
||||
}
|
||||
|
||||
// only pull the leads as filtered via DBAL
|
||||
$q->where(
|
||||
$q->expr()->in($this->getTableAlias().'.id', ':entityIds')
|
||||
)->setParameter('entityIds', $ids);
|
||||
|
||||
$results = $q->getQuery()
|
||||
->useQueryCache(false) // the query contains ID's, so there is no use in caching it
|
||||
->getResult();
|
||||
|
||||
// assign fields
|
||||
/** @var Lead $r */
|
||||
foreach ($results as $r) {
|
||||
$id = $r->getId();
|
||||
$r->setFields($fieldValues[$id]);
|
||||
|
||||
if (is_callable($resultsCallback)) {
|
||||
$resultsCallback($r);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$results = [];
|
||||
}
|
||||
}
|
||||
|
||||
return (!empty($args['withTotalCount'])) ?
|
||||
[
|
||||
'count' => $total,
|
||||
'results' => $results,
|
||||
] : $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $byGroup
|
||||
* @param string $object
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFieldValues($id, $byGroup = true, $object = 'lead')
|
||||
{
|
||||
// use DBAL to get entity fields
|
||||
$q = $this->getEntitiesDbalQueryBuilder();
|
||||
|
||||
if (is_array($id)) {
|
||||
$this->buildSelectClause($q, $id);
|
||||
$id = $id['id'];
|
||||
} else {
|
||||
$q->select($this->getTableAlias().'.*');
|
||||
}
|
||||
|
||||
$q->where($this->getTableAlias().'.id = '.(int) $id);
|
||||
$values = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return $this->formatFieldValues($values, $byGroup, $object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of unique values from fields for autocompletes.
|
||||
*
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getValueList($field, $search = '', $limit = 10, $start = 0)
|
||||
{
|
||||
// Includes prefix
|
||||
$table = $this->getEntityManager()->getClassMetadata($this->getClassName())->getTableName();
|
||||
$col = $this->getTableAlias().'.'.$field;
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select("DISTINCT $col")
|
||||
->from($table, 'l');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->neq($col, $q->expr()->literal('')),
|
||||
$q->expr()->isNotNull($col)
|
||||
)
|
||||
);
|
||||
|
||||
if (!empty($search)) {
|
||||
$q->andWhere("$col LIKE :search")
|
||||
->setParameter('search', "{$search}%");
|
||||
}
|
||||
|
||||
$q->orderBy($col);
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist an array of entities.
|
||||
*
|
||||
* @param array $entities
|
||||
*/
|
||||
public function saveEntities($entities): void
|
||||
{
|
||||
foreach ($entities as $entity) {
|
||||
// Leads cannot be batched due to requiring the ID to update the fields
|
||||
$this->saveEntity($entity);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveEntity($entity, $flush = true): void
|
||||
{
|
||||
$this->preSaveEntity($entity);
|
||||
|
||||
$this->getEntityManager()->persist($entity);
|
||||
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush($entity);
|
||||
}
|
||||
|
||||
// Includes prefix
|
||||
$table = $this->getEntityManager()->getClassMetadata($this->getClassName())->getTableName();
|
||||
$fields = $entity->getUpdatedFields();
|
||||
if (method_exists($entity, 'getChanges')) {
|
||||
$changes = $entity->getChanges();
|
||||
|
||||
// remove the fields that are part of changes as they were already saved via a setter
|
||||
$fields = array_diff_key($fields, $changes);
|
||||
}
|
||||
|
||||
$this->prepareDbalFieldsForSave($fields);
|
||||
|
||||
if (!empty($fields)) {
|
||||
$this->getEntityManager()->getConnection()->update($table, $fields, ['id' => $entity->getId()]);
|
||||
}
|
||||
|
||||
$this->postSaveEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to remove non custom field columns from an arrayed lead row.
|
||||
*
|
||||
* @param array $fixedFields
|
||||
*/
|
||||
protected function removeNonFieldColumns(&$r, $fixedFields = [])
|
||||
{
|
||||
$baseCols = $this->getBaseColumns($this->getClassName(), true);
|
||||
foreach ($baseCols as $c) {
|
||||
if (!isset($fixedFields[$c])) {
|
||||
unset($r[$c]);
|
||||
}
|
||||
}
|
||||
unset($r['owner_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @param bool $byGroup
|
||||
* @param string $object
|
||||
*/
|
||||
protected function formatFieldValues($values, $byGroup = true, $object = 'lead'): array
|
||||
{
|
||||
[$fields, $fixedFields] = $this->getCustomFieldList($object);
|
||||
|
||||
$this->removeNonFieldColumns($values, $fixedFields);
|
||||
|
||||
// Reorder leadValues based on field order
|
||||
$values = array_merge(array_flip(array_keys($fields)), $values);
|
||||
|
||||
$fieldValues = [];
|
||||
|
||||
// loop over results to put fields in something that can be assigned to the entities
|
||||
foreach ($values as $k => $r) {
|
||||
if (isset($fields[$k])) {
|
||||
$r = CustomFieldHelper::fixValueType($fields[$k]['type'], $r);
|
||||
|
||||
if (!is_null($r)) {
|
||||
switch ($fields[$k]['type']) {
|
||||
case 'number':
|
||||
$r = (float) $r;
|
||||
break;
|
||||
case 'boolean':
|
||||
$r = (int) $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$alias = $fields[$k]['alias'];
|
||||
|
||||
if ($byGroup) {
|
||||
$group = $fields[$k]['group'];
|
||||
$fieldValues[$group][$alias] = $fields[$k];
|
||||
$fieldValues[$group][$alias]['value'] = $r;
|
||||
} else {
|
||||
$fieldValues[$alias] = $fields[$k];
|
||||
$fieldValues[$alias]['value'] = $r;
|
||||
}
|
||||
|
||||
unset($fields[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($byGroup) {
|
||||
// make sure each group key is present
|
||||
$groups = $this->getFieldGroups();
|
||||
foreach ($groups as $g) {
|
||||
if (!isset($fieldValues[$g])) {
|
||||
$fieldValues[$g] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $fieldValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the aliases of searchable fields that are indexed and published.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getSearchableFieldAliases(LeadFieldRepository $leadFieldRepository, string $object): array
|
||||
{
|
||||
return $leadFieldRepository->getSearchableFieldAliases($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $object
|
||||
*
|
||||
* @return array [$fields, $fixedFields]
|
||||
*/
|
||||
public function getCustomFieldList($object)
|
||||
{
|
||||
if (empty($this->customFieldList)) {
|
||||
// Get the list of custom fields
|
||||
$results = $this->getFieldList($object);
|
||||
|
||||
$fields = [];
|
||||
$fixedFields = [];
|
||||
foreach ($results as $r) {
|
||||
$fields[$r['alias']] = $r;
|
||||
if ($r['is_fixed']) {
|
||||
$fixedFields[$r['alias']] = $r['alias'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->customFieldList = [$fields, $fixedFields];
|
||||
}
|
||||
|
||||
return $this->customFieldList;
|
||||
}
|
||||
|
||||
protected function prepareDbalFieldsForSave(&$fields)
|
||||
{
|
||||
// Ensure booleans are integers
|
||||
foreach ($fields as $field => &$value) {
|
||||
if (is_bool($value)) {
|
||||
$fields[$field] = (int) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<int|string>|int|string>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getFieldList(?string $object = null): array
|
||||
{
|
||||
// Get the list of custom fields
|
||||
$fq = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$fq->select(
|
||||
'f.id, f.label, f.alias, f.type, f.field_group as "group", f.object, f.is_fixed, f.properties, f.default_value'
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'f')
|
||||
->where('f.is_published = :published')
|
||||
->setParameter('published', true, 'boolean')
|
||||
->addOrderBy('f.field_order', 'asc');
|
||||
|
||||
if (null !== $object) {
|
||||
$fq->andWhere($fq->expr()->eq('object', ':object'))
|
||||
->setParameter('object', $object);
|
||||
}
|
||||
|
||||
return $fq->executeQuery()->fetchAllAssociative() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit and use in class if required to do something to the entity prior to persisting.
|
||||
*/
|
||||
protected function preSaveEntity($entity)
|
||||
{
|
||||
// Inherit and use if required
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit and use in class if required to do something with the entity after persisting.
|
||||
*/
|
||||
protected function postSaveEntity($entity)
|
||||
{
|
||||
// Inherit and use if required
|
||||
}
|
||||
|
||||
public function setUniqueIdentifiersOperator(string $uniqueIdentifiersOperator): void
|
||||
{
|
||||
$this->uniqueIdentifiersOperator = $uniqueIdentifiersOperator;
|
||||
}
|
||||
|
||||
public function getUniqueIdentifiersWherePart(): string
|
||||
{
|
||||
if ($this->uniqueIdentifiersOperatorIs(CompositeExpression::TYPE_AND)) {
|
||||
return 'andWhere';
|
||||
}
|
||||
|
||||
return 'orWhere';
|
||||
}
|
||||
|
||||
private function uniqueIdentifiersOperatorIs(string $operator): bool
|
||||
{
|
||||
return $this->uniqueIdentifiersOperator === $operator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
|
||||
class DoNotContact
|
||||
{
|
||||
/**
|
||||
* Lead is contactable.
|
||||
*/
|
||||
public const IS_CONTACTABLE = 0;
|
||||
|
||||
/**
|
||||
* Lead unsubscribed themselves.
|
||||
*/
|
||||
public const UNSUBSCRIBED = 1;
|
||||
|
||||
/**
|
||||
* Lead was unsubscribed due to an unsuccessful send.
|
||||
*/
|
||||
public const BOUNCED = 2;
|
||||
|
||||
/**
|
||||
* Lead was manually unsubscribed by user.
|
||||
*/
|
||||
public const MANUAL = 3;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $reason = 0;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $comments;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
private $channelId;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_donotcontact')
|
||||
->setCustomRepositoryClass(DoNotContactRepository::class)
|
||||
->addIndex(['lead_id', 'channel', 'reason'], 'leadid_reason_channel')
|
||||
->addIndex(['reason'], 'dnc_reason_search')
|
||||
->addIndex(['date_added'], 'dnc_date_added');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(true, 'CASCADE', false, 'doNotContact');
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('reason', 'smallint')
|
||||
->build();
|
||||
|
||||
$builder->createField('channel', 'string')
|
||||
->build();
|
||||
|
||||
$builder->addNamedField('channelId', 'integer', 'channel_id', true);
|
||||
|
||||
$builder->createField('comments', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('doNotContact')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'dateAdded',
|
||||
'reason',
|
||||
'comments',
|
||||
'channel',
|
||||
'channelId',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'lead',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setDateAdded(\DateTime $dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getReason()
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $reason
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setReason($reason)
|
||||
{
|
||||
$this->reason = $reason;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComments()
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comments
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setComments($comments)
|
||||
{
|
||||
$this->comments = InputHelper::string((string) $comments);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setChannel($channel)
|
||||
{
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $channelId
|
||||
*
|
||||
* @return DoNotContact
|
||||
*/
|
||||
public function setChannelId($channelId)
|
||||
{
|
||||
$this->channelId = $channelId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<DoNotContact>
|
||||
*/
|
||||
class DoNotContactRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Get a list of DNC entries based on channel and lead_id.
|
||||
*
|
||||
* @param string $channel
|
||||
*
|
||||
* @return DoNotContact[]
|
||||
*/
|
||||
public function getEntriesByLeadAndChannel(Lead $lead, $channel)
|
||||
{
|
||||
return $this->findBy(['channel' => $channel, 'lead' => $lead]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $channel
|
||||
* @param array<int,int|string>|int|null $ids
|
||||
* @param int|null $reason
|
||||
* @param array<int,int|string>|int|true|null $listId
|
||||
* @param bool $combined
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getCount($channel = null, $ids = null, $reason = null, $listId = null, ?ChartQuery $chartQuery = null, $combined = false)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(dnc.id) as dnc_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc');
|
||||
|
||||
if ($ids) {
|
||||
if (!is_array($ids)) {
|
||||
$ids = [(int) $ids];
|
||||
}
|
||||
$q->where(
|
||||
$q->expr()->in('dnc.channel_id', $ids)
|
||||
);
|
||||
}
|
||||
|
||||
if ($channel) {
|
||||
$q->andWhere('dnc.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
if ($reason) {
|
||||
$q->andWhere('dnc.reason = :reason')
|
||||
->setParameter('reason', $reason);
|
||||
}
|
||||
|
||||
if ($listId) {
|
||||
if (!$combined) {
|
||||
$q->innerJoin('dnc', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'cs', 'cs.lead_id = dnc.lead_id');
|
||||
|
||||
if (true === $listId && !$combined) {
|
||||
$q->addSelect('cs.leadlist_id')
|
||||
->groupBy('cs.leadlist_id');
|
||||
} elseif (is_array($listId)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('cs.leadlist_id', ':segmentIds')
|
||||
);
|
||||
|
||||
$q->setParameter('segmentIds', $listId, ArrayParameterType::INTEGER);
|
||||
|
||||
$q->addSelect('cs.leadlist_id')
|
||||
->groupBy('cs.leadlist_id');
|
||||
} else {
|
||||
$q->andWhere('cs.leadlist_id = :list_id')
|
||||
->setParameter('list_id', $listId);
|
||||
}
|
||||
} else {
|
||||
$subQ = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQ->select('distinct(list.lead_id)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'list')
|
||||
->andWhere(
|
||||
$q->expr()->in('list.leadlist_id', ':segmentIds')
|
||||
);
|
||||
|
||||
$q->setParameter('segmentIds', $listId, ArrayParameterType::INTEGER);
|
||||
|
||||
$q->innerJoin('dnc', sprintf('(%s)', $subQ->getSQL()), 'cs', 'cs.lead_id = dnc.lead_id');
|
||||
}
|
||||
}
|
||||
|
||||
if ($chartQuery) {
|
||||
$chartQuery->applyDateFilters($q, 'date_added', 'dnc');
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
if ((true === $listId || is_array($listId)) && !$combined) {
|
||||
// Return list group of counts
|
||||
$byList = [];
|
||||
foreach ($results as $result) {
|
||||
$byList[$result['leadlist_id']] = $result['dnc_count'];
|
||||
}
|
||||
|
||||
return $byList;
|
||||
}
|
||||
|
||||
return (isset($results[0])) ? $results[0]['dnc_count'] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTimelineStats($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select('dnc.id, dnc.channel, dnc.channel_id, dnc.date_added, dnc.reason, dnc.comments, dnc.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where($query->expr()->eq('dnc.lead_id', (int) $leadId));
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->like('dnc.channel', $query->expr()->literal('%'.$options['search'].'%'))
|
||||
);
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'dnc.channel', 'dnc.date_added', [], ['date_added'], null, 'dnc.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $channel
|
||||
* @param string[]|int[] $contacts Array of contact IDs to filter by
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getChannelList($channel, ?array $contacts = null): array
|
||||
{
|
||||
// If no contacts are sent then stop querying for all of the DNC records as it leads to the out of memory error.
|
||||
if (is_array($contacts) && empty($contacts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
|
||||
->leftJoin('dnc', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = dnc.lead_id');
|
||||
|
||||
if (null === $channel) {
|
||||
$q->select('dnc.channel, dnc.reason, l.id as lead_id');
|
||||
} else {
|
||||
$q->select('l.id, dnc.reason')
|
||||
->where('dnc.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
if ($contacts) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('l.id', $contacts)
|
||||
);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$dnc = [];
|
||||
foreach ($results as $r) {
|
||||
if (isset($r['lead_id'])) {
|
||||
if (!isset($dnc[$r['lead_id']])) {
|
||||
$dnc[$r['lead_id']] = [];
|
||||
}
|
||||
|
||||
$dnc[$r['lead_id']][$r['channel']] = $r['reason'];
|
||||
} else {
|
||||
$dnc[$r['id']] = $r['reason'];
|
||||
}
|
||||
}
|
||||
|
||||
unset($results);
|
||||
|
||||
return $dnc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique combinations of reason and channel.
|
||||
*
|
||||
* @return array<int, array{reason: mixed, channel: mixed}> Array of arrays containing 'reason' and 'channel'
|
||||
*/
|
||||
public function getReasonChannelCombinations(): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('dnc')
|
||||
->select('DISTINCT dnc.reason, dnc.channel')
|
||||
->orderBy('dnc.reason', 'ASC')
|
||||
->addOrderBy('dnc.channel', 'ASC');
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
trait ExpressionHelperTrait
|
||||
{
|
||||
/**
|
||||
* @param QueryBuilder|\Doctrine\ORM\QueryBuilder $q
|
||||
* @param $includeIsNull true/false or null to auto determine based on operator
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull, ?CompositeExpression $appendTo = null)
|
||||
{
|
||||
// in/notIn for dbal will use a raw array
|
||||
if (!is_array($parameter) && !str_starts_with($parameter, ':')) {
|
||||
$parameter = ":$parameter";
|
||||
}
|
||||
|
||||
if (null === $includeIsNull) {
|
||||
// Auto determine based on negate operators
|
||||
$includeIsNull = in_array($operator, ['neq', 'notLike', 'notIn']);
|
||||
}
|
||||
|
||||
if ($includeIsNull) {
|
||||
$expr = $q->expr()->or(
|
||||
$q->expr()->$operator($column, $parameter),
|
||||
$q->expr()->isNull($column)
|
||||
);
|
||||
} else {
|
||||
$expr = $q->expr()->$operator($column, $parameter);
|
||||
}
|
||||
|
||||
if ($appendTo) {
|
||||
return $appendTo->with($expr);
|
||||
}
|
||||
|
||||
return $expr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonEntity;
|
||||
|
||||
class FrequencyRule extends CommonEntity
|
||||
{
|
||||
public const TIME_DAY = 'DAY';
|
||||
|
||||
public const TIME_WEEK = 'WEEK';
|
||||
|
||||
public const TIME_MONTH = 'MONTH';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $frequencyNumber;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $frequencyTime;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $preferredChannel = 0;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $pauseFromDate;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $pauseToDate;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_frequencyrules')
|
||||
->setCustomRepositoryClass(FrequencyRuleRepository::class)
|
||||
->addIndex(['channel'], 'channel_frequency')
|
||||
->addIndex(['lead_id', 'date_added'], 'idx_frequency_date_added');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'frequencyRules');
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->addNamedField('frequencyNumber', 'smallint', 'frequency_number', true);
|
||||
|
||||
$builder->createField('frequencyTime', 'string')
|
||||
->columnName('frequency_time')
|
||||
->nullable()
|
||||
->length(25)
|
||||
->build();
|
||||
|
||||
$builder->createField('channel', 'string')
|
||||
->build();
|
||||
|
||||
$builder->createField('preferredChannel', 'boolean')
|
||||
->columnName('preferred_channel')
|
||||
->build();
|
||||
|
||||
$builder->createField('pauseFromDate', 'datetime')
|
||||
->columnName('pause_from_date')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('pauseToDate', 'datetime')
|
||||
->columnName('pause_to_date')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('frequencyRules')
|
||||
->addListProperties(
|
||||
[
|
||||
'channel',
|
||||
'frequencyNumber',
|
||||
'frequencyTime',
|
||||
'preferredChannel',
|
||||
'pauseFromDate',
|
||||
'pauseToDate',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'lead',
|
||||
'dateAdded',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setLead($lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeInterface $dateAdded
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->isChanged('dateAdded', $dateAdded);
|
||||
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getFrequencyNumber()
|
||||
{
|
||||
return $this->frequencyNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $frequencyNumber
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setFrequencyNumber($frequencyNumber)
|
||||
{
|
||||
$this->isChanged('frequencyNumber', $frequencyNumber);
|
||||
|
||||
$this->frequencyNumber = $frequencyNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFrequencyTime()
|
||||
{
|
||||
return $this->frequencyTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $frequencyTime
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setFrequencyTime($frequencyTime)
|
||||
{
|
||||
$this->isChanged('frequencyTime', $frequencyTime);
|
||||
|
||||
$this->frequencyTime = $frequencyTime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setChannel($channel)
|
||||
{
|
||||
$this->isChanged('channel', $channel);
|
||||
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isPreferredChannel()
|
||||
{
|
||||
return $this->preferredChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getPreferredChannel()
|
||||
{
|
||||
return $this->preferredChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $preferredChannel
|
||||
*
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setPreferredChannel($preferredChannel)
|
||||
{
|
||||
$this->isChanged('preferredChannel', $preferredChannel);
|
||||
|
||||
$this->preferredChannel = $preferredChannel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPauseFromDate()
|
||||
{
|
||||
return $this->pauseFromDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setPauseFromDate(?\DateTime $pauseFromDate = null)
|
||||
{
|
||||
$this->isChanged('pauseFromDate', $pauseFromDate);
|
||||
|
||||
$this->pauseFromDate = $pauseFromDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPauseToDate()
|
||||
{
|
||||
return $this->pauseToDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FrequencyRule
|
||||
*/
|
||||
public function setPauseToDate(?\DateTime $pauseToDate = null)
|
||||
{
|
||||
$this->isChanged('pauseToDate', $pauseToDate);
|
||||
|
||||
$this->pauseToDate = $pauseToDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<FrequencyRule>
|
||||
*/
|
||||
class FrequencyRuleRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @param string $channel
|
||||
* @param array $leadIds
|
||||
* @param string|null $defaultFrequencyNumber
|
||||
* @param string|null $defaultFrequencyTime
|
||||
* @param string $statTable
|
||||
* @param string $statSentColumn
|
||||
* @param string $statContactColumn
|
||||
*/
|
||||
public function getAppliedFrequencyRules(
|
||||
$channel,
|
||||
$leadIds,
|
||||
$defaultFrequencyNumber,
|
||||
$defaultFrequencyTime,
|
||||
$statTable = 'email_stats',
|
||||
$statContactColumn = 'lead_id',
|
||||
$statSentColumn = 'date_sent',
|
||||
): array {
|
||||
if (empty($leadIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$frequencyRuleViolations = $this->getCustomFrequencyRuleViolations($channel, $leadIds, $statTable, $statContactColumn, $statSentColumn);
|
||||
|
||||
if (!$this->validateDefaultParameters($defaultFrequencyNumber, $defaultFrequencyTime)) {
|
||||
// It makes no sense to calculate default rule violations
|
||||
// if default parameters are not valid
|
||||
return $frequencyRuleViolations;
|
||||
}
|
||||
|
||||
$defaultRuleViolations = $this->getDefaultFrequencyRuleViolations($leadIds, $defaultFrequencyNumber, $defaultFrequencyTime, $statTable, $statContactColumn, $statSentColumn);
|
||||
|
||||
return array_merge($frequencyRuleViolations, $defaultRuleViolations);
|
||||
}
|
||||
|
||||
private function validateDefaultParameters(mixed $number, mixed $time): bool
|
||||
{
|
||||
return $number && $time;
|
||||
}
|
||||
|
||||
public function getFrequencyRules($channel = null, $leadIds = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select(
|
||||
'fr.id, fr.frequency_time, fr.frequency_number, fr.channel, fr.preferred_channel, fr.pause_from_date, fr.pause_to_date, fr.lead_id'
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr');
|
||||
|
||||
if ($channel) {
|
||||
$q->andWhere('fr.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
$groupByLeads = is_array($leadIds);
|
||||
if ($leadIds) {
|
||||
if ($groupByLeads) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('fr.lead_id', $leadIds)
|
||||
);
|
||||
} else {
|
||||
$q->andWhere('fr.lead_id = :leadId')
|
||||
->setParameter('leadId', (int) $leadIds);
|
||||
}
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$frequencyRules = [];
|
||||
|
||||
foreach ($results as $result) {
|
||||
if ($groupByLeads) {
|
||||
if (!isset($frequencyRules[$result['lead_id']])) {
|
||||
$frequencyRules[$result['lead_id']] = [];
|
||||
}
|
||||
|
||||
$frequencyRules[$result['lead_id']][$result['channel']] = $result;
|
||||
} else {
|
||||
$frequencyRules[$result['channel']] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $frequencyRules;
|
||||
}
|
||||
|
||||
public function getPreferredChannel($leadId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('fr.id, fr.frequency_time, fr.frequency_number, fr.channel, fr.pause_from_date, fr.pause_to_date')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr');
|
||||
$q->where('fr.preferred_channel = :preferredChannel')
|
||||
->setParameter('preferredChannel', true, 'boolean');
|
||||
if ($leadId) {
|
||||
$q->andWhere('fr.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
* @param string $statTable
|
||||
* @param string $statContactColumn
|
||||
* @param string $statSentColumn
|
||||
*/
|
||||
private function getCustomFrequencyRuleViolations($channel, array $leadIds, $statTable, $statContactColumn, $statSentColumn): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select("ch.$statContactColumn, fr.frequency_number, fr.frequency_time")
|
||||
->from(MAUTIC_TABLE_PREFIX.$statTable, 'ch')
|
||||
->join('ch', MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr', "ch.{$statContactColumn} = fr.lead_id");
|
||||
|
||||
if ($channel) {
|
||||
$q->andWhere('fr.channel = :channel')
|
||||
->setParameter('channel', $channel);
|
||||
}
|
||||
|
||||
// Preferred channel is stored in this table so they may not have a frequency rule defined but just a preference so exclude them
|
||||
$q->andWhere('fr.frequency_time IS NOT NULL AND fr.frequency_number IS NOT NULL');
|
||||
|
||||
// Calculate the rule timeframe
|
||||
$q->andWhere(
|
||||
'(ch.'.$statSentColumn.' >= case fr.frequency_time
|
||||
when \'MONTH\' then DATE_SUB(NOW(),INTERVAL 1 MONTH)
|
||||
when \'DAY\' then DATE_SUB(NOW(),INTERVAL 1 DAY)
|
||||
when \'WEEK\' then DATE_SUB(NOW(),INTERVAL 1 WEEK)
|
||||
end)'
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
$q->expr()->in("ch.$statContactColumn", $leadIds)
|
||||
);
|
||||
|
||||
$q->groupBy("ch.$statContactColumn, fr.frequency_time, fr.frequency_number");
|
||||
|
||||
$q->having("count(ch.$statContactColumn) >= fr.frequency_number");
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $defaultFrequencyNumber
|
||||
* @param string $defaultFrequencyTime
|
||||
* @param string $statTable
|
||||
* @param string $statContactColumn
|
||||
* @param string $statSentColumn
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getDefaultFrequencyRuleViolations(
|
||||
array $leadIds,
|
||||
$defaultFrequencyNumber,
|
||||
$defaultFrequencyTime,
|
||||
$statTable,
|
||||
$statContactColumn,
|
||||
$statSentColumn,
|
||||
) {
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select("ch.$statContactColumn")
|
||||
->from(MAUTIC_TABLE_PREFIX.$statTable, 'ch');
|
||||
|
||||
switch ($defaultFrequencyTime) {
|
||||
case 'MONTH':
|
||||
$since = new \DateTime('-1 month', new \DateTimeZone('UTC'));
|
||||
break;
|
||||
case 'WEEK':
|
||||
$since = new \DateTime('-1 week', new \DateTimeZone('UTC'));
|
||||
break;
|
||||
case 'DAY':
|
||||
$since = new \DateTime('-1 day', new \DateTimeZone('UTC'));
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
$query->andWhere('ch.'.$statSentColumn.' >= :frequencyTime')
|
||||
->setParameter('frequencyTime', $since->format('Y-m-d H:i:s'));
|
||||
|
||||
$query->andWhere(
|
||||
$query->expr()->in("ch.$statContactColumn", $leadIds)
|
||||
);
|
||||
|
||||
$hasCustomRules = $this->tableHasRows(MAUTIC_TABLE_PREFIX.'lead_frequencyrules');
|
||||
// We don't need to check if users have custom rules if there are no records inside that table
|
||||
if ($hasCustomRules) {
|
||||
// Exclude contacts with custom rules defined
|
||||
$subQuery = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQuery->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_frequencyrules', 'fr')
|
||||
->where("fr.lead_id = ch.{$statContactColumn}")
|
||||
->andWhere('fr.frequency_time IS NOT NULL AND fr.frequency_number IS NOT NULL');
|
||||
$query->andWhere(
|
||||
sprintf('NOT EXISTS (%s)', $subQuery->getSQL())
|
||||
);
|
||||
}
|
||||
|
||||
$query->groupBy("ch.$statContactColumn");
|
||||
|
||||
$query->having("count(ch.$statContactColumn) >= :defaultNumber")
|
||||
->setParameter('defaultNumber', $defaultFrequencyNumber);
|
||||
|
||||
$results = $query->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $key => $result) {
|
||||
$results[$key]['frequency_number'] = $defaultFrequencyNumber;
|
||||
$results[$key]['frequency_time'] = $defaultFrequencyTime;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
interface IdentifierFieldEntityInterface
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getDefaultIdentifierFields(): array;
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Helper\Chart\PieChart;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
class Import extends FormEntity
|
||||
{
|
||||
/** ===== Statuses: ===== */
|
||||
/**
|
||||
* When the import entity is created for background processing.
|
||||
*/
|
||||
public const QUEUED = 1;
|
||||
|
||||
/**
|
||||
* When the background process started the import.
|
||||
*/
|
||||
public const IN_PROGRESS = 2;
|
||||
|
||||
/**
|
||||
* When the import is finished.
|
||||
*/
|
||||
public const IMPORTED = 3;
|
||||
|
||||
/**
|
||||
* When the import process failed.
|
||||
*/
|
||||
public const FAILED = 4;
|
||||
|
||||
/**
|
||||
* When the import has been stopped by a user.
|
||||
*/
|
||||
public const STOPPED = 5;
|
||||
|
||||
/**
|
||||
* When the import happens in the browser.
|
||||
*/
|
||||
public const MANUAL = 6;
|
||||
|
||||
/**
|
||||
* When the import is scheduled for later processing.
|
||||
*/
|
||||
public const DELAYED = 7;
|
||||
|
||||
/**
|
||||
* ===== Priorities: =====.
|
||||
*/
|
||||
public const LOW = 512;
|
||||
|
||||
public const NORMAL = 64;
|
||||
|
||||
public const HIGH = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* Base directory of the import.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dir;
|
||||
|
||||
/**
|
||||
* File name of the CSV file which is in the $dir.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $file = 'import.csv';
|
||||
|
||||
/**
|
||||
* Name of the original uploaded file.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $originalFile;
|
||||
|
||||
/**
|
||||
* Tolal line count of the CSV file.
|
||||
*/
|
||||
private int $lineCount = 0;
|
||||
|
||||
/**
|
||||
* Count of entities which were newly created.
|
||||
*/
|
||||
private int $insertedCount = 0;
|
||||
|
||||
/**
|
||||
* Count of entities which were updated.
|
||||
*/
|
||||
private int $updatedCount = 0;
|
||||
|
||||
/**
|
||||
* Count of ignored items.
|
||||
*/
|
||||
private int $ignoredCount = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $priority;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $status;
|
||||
|
||||
private ?\DateTimeInterface $dateStarted = null;
|
||||
|
||||
private ?\DateTimeInterface $dateEnded = null;
|
||||
|
||||
private string $object = 'lead';
|
||||
|
||||
/**
|
||||
* @var array<mixed>|null
|
||||
*/
|
||||
private $properties = [];
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->status = self::QUEUED;
|
||||
$this->priority = self::LOW;
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('imports')
|
||||
->setCustomRepositoryClass(ImportRepository::class)
|
||||
->addIndex(['object'], 'import_object')
|
||||
->addIndex(['status'], 'import_status')
|
||||
->addIndex(['priority'], 'import_priority')
|
||||
->addId()
|
||||
->addField('dir', Types::STRING)
|
||||
->addField('file', Types::STRING)
|
||||
->addNullableField('originalFile', Types::STRING, 'original_file')
|
||||
->addNamedField('lineCount', Types::INTEGER, 'line_count')
|
||||
->addNamedField('insertedCount', Types::INTEGER, 'inserted_count')
|
||||
->addNamedField('updatedCount', Types::INTEGER, 'updated_count')
|
||||
->addNamedField('ignoredCount', Types::INTEGER, 'ignored_count')
|
||||
->addField('priority', Types::INTEGER)
|
||||
->addField('status', Types::INTEGER)
|
||||
->addNullableField('dateStarted', Types::DATETIME_MUTABLE, 'date_started')
|
||||
->addNullableField('dateEnded', Types::DATETIME_MUTABLE, 'date_ended')
|
||||
->addField('object', Types::STRING)
|
||||
->addNullableField('properties', Types::JSON);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('dir', new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.import.dir.notblank']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('file', new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.import.file.notblank']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('import')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'dir',
|
||||
'file',
|
||||
'originalFile',
|
||||
'lineCount',
|
||||
'insertedCount',
|
||||
'updatedCount',
|
||||
'ignoredCount',
|
||||
'priority',
|
||||
'status',
|
||||
'dateStarted',
|
||||
'dateEnded',
|
||||
'object',
|
||||
'properties',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the import has everything needed to proceed.
|
||||
*/
|
||||
public function canProceed(): bool
|
||||
{
|
||||
if (!in_array($this->getStatus(), [self::QUEUED, self::DELAYED])) {
|
||||
$this->setStatusInfo('Import could not be triggered since it is not queued nor delayed');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === file_exists($this->getFilePath()) || false === is_readable($this->getFilePath())) {
|
||||
$this->setStatus(self::FAILED);
|
||||
$this->setStatusInfo($this->getFile().' not found');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides if this import entity is triggered as the background
|
||||
* job or as UI process.
|
||||
*/
|
||||
public function isBackgroundProcess(): bool
|
||||
{
|
||||
return !(self::MANUAL === $this->getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setDir($dir)
|
||||
{
|
||||
$this->isChanged('dir', $dir);
|
||||
$this->dir = $dir;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDir()
|
||||
{
|
||||
return $this->dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setFile($file)
|
||||
{
|
||||
$this->isChanged('file', $file);
|
||||
$this->file = $file;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFile()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get import file path.
|
||||
*/
|
||||
public function getFilePath(): string
|
||||
{
|
||||
return $this->getDir().'/'.$this->getFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set import file path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setFilePath($path)
|
||||
{
|
||||
$fileName = basename($path);
|
||||
$dir = substr($path, 0, -1 * (strlen($fileName) + 1));
|
||||
|
||||
$this->setDir($dir);
|
||||
$this->setFile($fileName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file if exists.
|
||||
* It won't throw any exception if the file is not readable.
|
||||
* Not removing the CSV file is not considered a big trouble.
|
||||
* It will be removed on the next cache:clear.
|
||||
*/
|
||||
public function removeFile(): void
|
||||
{
|
||||
$file = $this->getFilePath();
|
||||
|
||||
if (file_exists($file) && is_writable($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $originalFile
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setOriginalFile($originalFile)
|
||||
{
|
||||
$this->isChanged('originalFile', $originalFile);
|
||||
$this->originalFile = $originalFile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOriginalFile()
|
||||
{
|
||||
return $this->originalFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* getName method is used by standard templates so there it is for this entity.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->getOriginalFile() ?: $this->getId();
|
||||
}
|
||||
|
||||
public function setLineCount(int $lineCount): self
|
||||
{
|
||||
$this->isChanged('lineCount', $lineCount);
|
||||
$this->lineCount = $lineCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLineCount(): int
|
||||
{
|
||||
return $this->lineCount;
|
||||
}
|
||||
|
||||
public function setInsertedCount(int $insertedCount): self
|
||||
{
|
||||
$this->isChanged('insertedCount', $insertedCount);
|
||||
$this->insertedCount = $insertedCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function increaseInsertedCount(): self
|
||||
{
|
||||
return $this->setInsertedCount($this->insertedCount + 1);
|
||||
}
|
||||
|
||||
public function getInsertedCount(): int
|
||||
{
|
||||
return $this->insertedCount;
|
||||
}
|
||||
|
||||
public function setUpdatedCount(int $updatedCount): self
|
||||
{
|
||||
$this->isChanged('updatedCount', $updatedCount);
|
||||
$this->updatedCount = $updatedCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function increaseUpdatedCount(): self
|
||||
{
|
||||
return $this->setUpdatedCount($this->updatedCount + 1);
|
||||
}
|
||||
|
||||
public function getUpdatedCount(): int
|
||||
{
|
||||
return $this->updatedCount;
|
||||
}
|
||||
|
||||
public function setIgnoredCount(int $ignoredCount): self
|
||||
{
|
||||
$this->isChanged('ignoredCount', $ignoredCount);
|
||||
$this->ignoredCount = $ignoredCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function increaseIgnoredCount(): self
|
||||
{
|
||||
return $this->setIgnoredCount($this->ignoredCount + 1);
|
||||
}
|
||||
|
||||
public function getIgnoredCount(): int
|
||||
{
|
||||
return $this->ignoredCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how many rows have been processed so far.
|
||||
*/
|
||||
public function getProcessedRows(): int
|
||||
{
|
||||
return $this->getInsertedCount() + $this->getUpdatedCount() + $this->getIgnoredCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts current progress percentage.
|
||||
*/
|
||||
public function getProgressPercentage(): float|int
|
||||
{
|
||||
$processed = $this->getProcessedRows();
|
||||
|
||||
if ($processed && $total = $this->getLineCount()) {
|
||||
return round(($processed / $total) * 100, 2);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $priority
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setPriority($priority)
|
||||
{
|
||||
$this->isChanged('priority', $priority);
|
||||
$this->priority = $priority;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority()
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $status
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
$this->isChanged('status', $status);
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Twitter Bootstrap label class based on current status.
|
||||
*/
|
||||
public function getSatusLabelClass(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
self::QUEUED => 'info',
|
||||
self::IN_PROGRESS, self::MANUAL => 'primary',
|
||||
self::IMPORTED => 'success',
|
||||
self::FAILED => 'danger',
|
||||
self::STOPPED, self::DELAYED => 'warning',
|
||||
default => 'default',
|
||||
};
|
||||
}
|
||||
|
||||
public function setDateStarted(?\DateTimeInterface $dateStarted): self
|
||||
{
|
||||
$this->isChanged('dateStarted', $dateStarted);
|
||||
$this->dateStarted = $dateStarted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateStarted(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dateStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the entity for the start of import.
|
||||
*/
|
||||
public function start(): self
|
||||
{
|
||||
if (empty($this->getDateStarted())) {
|
||||
$this->setDateStarted(new \DateTime());
|
||||
}
|
||||
|
||||
$this->setStatus(self::IN_PROGRESS);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the entity for the end of import.
|
||||
*/
|
||||
public function end($removeFile = true): self
|
||||
{
|
||||
$this->setDateEnded(new \DateTime());
|
||||
|
||||
if (self::IN_PROGRESS === $this->getStatus()) {
|
||||
$this->setStatus(self::IMPORTED);
|
||||
|
||||
if ($removeFile) {
|
||||
$this->removeFile();
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDateEnded(?\DateTimeInterface $dateEnded): self
|
||||
{
|
||||
$this->isChanged('dateEnded', $dateEnded);
|
||||
$this->dateEnded = $dateEnded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateEnded(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dateEnded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how long the import has run so far.
|
||||
*
|
||||
* @return \DateInterval|null
|
||||
*/
|
||||
public function getRunTime()
|
||||
{
|
||||
$startTime = $this->getDateStarted();
|
||||
$endTime = $this->getDateEnded();
|
||||
|
||||
if (!$endTime && self::IN_PROGRESS === $this->getStatus()) {
|
||||
$endTime = $this->getDateModified();
|
||||
}
|
||||
|
||||
if ($startTime instanceof \DateTimeInterface && $endTime instanceof \DateTimeInterface) {
|
||||
return $endTime->diff($startTime);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns run time in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRunTimeSeconds()
|
||||
{
|
||||
$startTime = $this->getDateStarted();
|
||||
$endTime = $this->getDateEnded();
|
||||
|
||||
if (!$endTime && self::IN_PROGRESS === $this->getStatus()) {
|
||||
$endTime = $this->getDateModified();
|
||||
}
|
||||
|
||||
if ($startTime instanceof \DateTime && $endTime instanceof \DateTime) {
|
||||
return $endTime->format('U') - $startTime->format('U');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts speed in items per second.
|
||||
*/
|
||||
public function getSpeed(): float
|
||||
{
|
||||
$runtime = $this->getRunTimeSeconds();
|
||||
$processedRows = $this->getProcessedRows();
|
||||
|
||||
if ($runtime && $processedRows) {
|
||||
return round($processedRows / $runtime, 2);
|
||||
}
|
||||
|
||||
return (float) $processedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $object
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setObject($object)
|
||||
{
|
||||
$this->isChanged('object', $object);
|
||||
$this->object = $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Import
|
||||
*/
|
||||
public function setMatchedFields(array $fields)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['fields'] = $fields;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
public function setLastLineImported($line): void
|
||||
{
|
||||
$this->properties['line'] = (int) $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLastLineImported()
|
||||
{
|
||||
return $this->properties['line'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMatchedFields()
|
||||
{
|
||||
return empty($this->properties['fields']) ? [] : $this->properties['fields'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $properties
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setProperties($properties)
|
||||
{
|
||||
$this->isChanged('properties', $properties);
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $properties
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function mergeToProperties($properties)
|
||||
{
|
||||
return $this->setProperties(array_merge($this->properties, $properties));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of default values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaults()
|
||||
{
|
||||
return $this->properties['defaults'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a default value to the defaults array.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setDefault($key, $value)
|
||||
{
|
||||
return $this->mergeToProperties([
|
||||
'defaults' => array_merge($this->getDefaults(), [$key => $value]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDefault($key)
|
||||
{
|
||||
return empty($this->properties['defaults'][$key]) ? null : $this->properties['defaults'][$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set headers array to the properties.
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setHeaders(array $headers)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['headers'] = $headers;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
return empty($this->properties['headers']) ? [] : $this->properties['headers'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parser config array to the properties.
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setParserConfig(array $parser)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['parser'] = $parser;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParserConfig()
|
||||
{
|
||||
return empty($this->properties['parser']) ? [] : $this->properties['parser'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStatusInfo()
|
||||
{
|
||||
return empty($this->properties['status_info']) ? 'unknown' : $this->properties['status_info'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $info
|
||||
*
|
||||
* @return Import
|
||||
*/
|
||||
public function setStatusInfo($info)
|
||||
{
|
||||
$properties = $this->properties;
|
||||
$properties['status_info'] = $info;
|
||||
|
||||
return $this->setProperties($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite this method so we could change import status based on it.
|
||||
*
|
||||
* @param bool $isPublished
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIsPublished($isPublished)
|
||||
{
|
||||
if ($isPublished && self::STOPPED === $this->getStatus()) {
|
||||
$this->setStatus(self::QUEUED);
|
||||
}
|
||||
|
||||
if (!$isPublished && (self::IN_PROGRESS === $this->getStatus() || self::QUEUED === $this->getStatus())) {
|
||||
$this->setStatus(self::STOPPED);
|
||||
}
|
||||
|
||||
return parent::setIsPublished($isPublished);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pie graph data for row status counts.
|
||||
*
|
||||
* @return array{labels: mixed[], datasets: mixed[]}
|
||||
*/
|
||||
public function getRowStatusesPieChart(Translator $translator): array
|
||||
{
|
||||
$chart = new PieChart();
|
||||
$chart->setDataset($translator->trans('mautic.lead.import.inserted.count'), $this->getInsertedCount());
|
||||
$chart->setDataset($translator->trans('mautic.lead.import.updated.count'), $this->getUpdatedCount());
|
||||
$chart->setDataset($translator->trans('mautic.lead.import.ignored.count'), $this->getIgnoredCount());
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Import>
|
||||
*/
|
||||
class ImportRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Count how many imports with the status is there.
|
||||
*
|
||||
* @param float $ghostDelay when is the import ghost? In hours
|
||||
* @param int $limit
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGhostImports($ghostDelay = 2, $limit = null)
|
||||
{
|
||||
$q = $this->getQueryForStatuses([Import::IN_PROGRESS]);
|
||||
$q->select($this->getTableAlias())
|
||||
->andWhere($q->expr()->lt($this->getTableAlias().'.dateModified', ':delay'))
|
||||
->setParameter('delay', (new \DateTime())->modify('-'.$ghostDelay.' hours'));
|
||||
|
||||
if (null !== $limit) {
|
||||
$q->setFirstResult(0)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many imports with the status is there.
|
||||
*
|
||||
* @param int $limit
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getImportsWithStatuses(array $statuses, $limit = null)
|
||||
{
|
||||
$q = $this->getQueryForStatuses($statuses);
|
||||
$q->select($this->getTableAlias())
|
||||
->orderBy($this->getTableAlias().'.priority', 'ASC')
|
||||
->addOrderBy($this->getTableAlias().'.dateAdded', 'DESC');
|
||||
|
||||
if (null !== $limit) {
|
||||
$q->setFirstResult(0)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many imports with the status is there.
|
||||
*/
|
||||
public function countImportsWithStatuses(array $statuses): int
|
||||
{
|
||||
$q = $this->getQueryForStatuses($statuses);
|
||||
$q->select('COUNT(DISTINCT '.$this->getTableAlias().'.id) as theCount');
|
||||
|
||||
$results = $q->getQuery()->getSingleResult();
|
||||
|
||||
if (isset($results['theCount'])) {
|
||||
return (int) $results['theCount'];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function countImportsInProgress(): int
|
||||
{
|
||||
return $this->countImportsWithStatuses([Import::IN_PROGRESS]);
|
||||
}
|
||||
|
||||
public function getQueryForStatuses($statuses)
|
||||
{
|
||||
$q = $this->createQueryBuilder($this->getTableAlias());
|
||||
|
||||
return $q->where($q->expr()->in($this->getTableAlias().'.status', $statuses));
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'i';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Contact Category',
|
||||
operations: [
|
||||
new GetCollection(uriTemplate: '/contactcategories', security: "is_granted('lead:leads:viewown')"),
|
||||
new Post(uriTemplate: '/contactcategories', security: "is_granted('lead:leads:create')"),
|
||||
new Get(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:viewown')"),
|
||||
new Put(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:editown')"),
|
||||
new Patch(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:editother')"),
|
||||
new Delete(uriTemplate: '/contactcategories/{id}', security: "is_granted('lead:leads:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['leadcategory:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['leadcategory:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class LeadCategory
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['leadcategory:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Category
|
||||
**/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $manuallyRemoved = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadcategory:read', 'leadcategory:write'])]
|
||||
private $manuallyAdded = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_categories')
|
||||
->setCustomRepositoryClass(LeadCategoryRepository::class);
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->createManyToOne('category', Category::class)
|
||||
->addJoinColumn('category_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('manuallyRemoved', 'boolean')
|
||||
->columnName('manually_removed')
|
||||
->build();
|
||||
|
||||
$builder->createField('manuallyAdded', 'boolean')
|
||||
->columnName('manually_added')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
*/
|
||||
public function setDateAdded($date): void
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Category
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Category $category
|
||||
*/
|
||||
public function setCategory($category): void
|
||||
{
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyRemoved
|
||||
*/
|
||||
public function setManuallyRemoved($manuallyRemoved): void
|
||||
{
|
||||
$this->manuallyRemoved = $manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyAdded
|
||||
*/
|
||||
public function setManuallyAdded($manuallyAdded): void
|
||||
{
|
||||
$this->manuallyAdded = $manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadCategory>
|
||||
*/
|
||||
class LeadCategoryRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @return array<mixed, array<string, mixed>>
|
||||
*/
|
||||
public function getLeadCategories(Lead $lead): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('lc.id, lc.category_id, lc.date_added, lc.manually_added, lc.manually_removed, c.alias, c.title')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_categories', 'lc')
|
||||
->join('lc', MAUTIC_TABLE_PREFIX.'categories', 'c', 'c.id = lc.category_id')
|
||||
->where('lc.lead_id = :lead')
|
||||
->andWhere('lc.manually_removed = 0')
|
||||
->setParameter('lead', $lead->getId());
|
||||
|
||||
$results = $q->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$categories = [];
|
||||
foreach ($results as $category) {
|
||||
$categories[$category['category_id']] = $category;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getUnsubscribedLeadCategories(Lead $lead): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('lc.id, lc.category_id, lc.date_added, lc.manually_added, lc.manually_removed, c.alias, c.title')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_categories', 'lc')
|
||||
->join('lc', MAUTIC_TABLE_PREFIX.'categories', 'c', 'c.id = lc.category_id')
|
||||
->where('lc.lead_id = :lead')
|
||||
->andWhere('lc.manually_removed = 1')
|
||||
->setParameter('lead', $lead->getId());
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$categories = [];
|
||||
foreach ($results as $category) {
|
||||
$categories[$category['category_id']] = $category;
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getSubscribedAndNewCategoryIds(Lead $lead, array $types): array
|
||||
{
|
||||
$criteria = Criteria::create()
|
||||
->andWhere(Criteria::expr()->eq('manuallyRemoved', 1));
|
||||
|
||||
return $this->getLeadCategoriesMapping($lead, $types, $criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getNonAssociatedCategoryIdsForAContact(Lead $lead, array $types): array
|
||||
{
|
||||
return $this->getLeadCategoriesMapping($lead, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $types
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function getLeadCategoriesMapping(Lead $lead, array $types, ?Criteria $criteria = null): array
|
||||
{
|
||||
$parentQ = $this->getEntityManager()->getRepository(Category::class)->createQueryBuilder('c');
|
||||
$parentQ->select('c.id');
|
||||
$parentQ->where('c.isPublished = :isPublished');
|
||||
$parentQ->setParameter('isPublished', 1);
|
||||
$parentQ->andWhere($parentQ->expr()->in('c.bundle', ':bundles'));
|
||||
$parentQ->setParameter('bundles', $types, ArrayParameterType::STRING);
|
||||
|
||||
// Get the category ids for particular lead
|
||||
$subQ = $this->getEntityManager()->getRepository(LeadCategory::class)->createQueryBuilder('lc');
|
||||
$subQ->select('IDENTITY(lc.category)');
|
||||
$subQ->where($subQ->expr()->eq('lc.lead', ':leadId'));
|
||||
$subQ->setParameter('leadId', $lead->getId());
|
||||
|
||||
if ($criteria) {
|
||||
$subQ->addCriteria($criteria);
|
||||
}
|
||||
|
||||
// Add sub-query
|
||||
$parentQ->andWhere($parentQ->expr()->notIn('c.id', $subQ->getDQL()));
|
||||
|
||||
// Add sub-query parameter.
|
||||
foreach ($subQ->getParameters() as $parameter) {
|
||||
$parentQ->setParameter($parameter->getName(), $parameter->getValue(), $parameter->getType());
|
||||
}
|
||||
|
||||
$leadCategories = $parentQ->getQuery()->getResult();
|
||||
|
||||
$leadCategoryList = [];
|
||||
foreach ($leadCategories as $category) {
|
||||
$id = (int) $category['id'];
|
||||
|
||||
$leadCategoryList[$id] = $id;
|
||||
}
|
||||
|
||||
return $leadCategoryList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class LeadDevice
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $clientInfo = [];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $device;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsShortName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsVersion;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceOsPlatform;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceBrand;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $deviceModel;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $trackingId;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_devices')
|
||||
->setCustomRepositoryClass(LeadDeviceRepository::class)
|
||||
->addIndex(['date_added'], 'date_added_search')
|
||||
->addIndex(['device'], 'device_search')
|
||||
->addIndex(['device_os_name'], 'device_os_name_search')
|
||||
->addIndex(['device_os_shortname'], 'device_os_shortname_search')
|
||||
->addIndex(['device_os_version'], 'device_os_version_search')
|
||||
->addIndex(['device_os_platform'], 'device_os_platform_search')
|
||||
->addIndex(['device_brand'], 'device_brand_search')
|
||||
->addIndex(['device_model'], 'device_model_search');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('clientInfo', 'array')
|
||||
->columnName('client_info')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('device', 'string');
|
||||
|
||||
$builder->createField('deviceOsName', 'string')
|
||||
->columnName('device_os_name')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceOsShortName', 'string')
|
||||
->columnName('device_os_shortname')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceOsVersion', 'string')
|
||||
->columnName('device_os_version')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceOsPlatform', 'string')
|
||||
->columnName('device_os_platform')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceBrand', 'string')
|
||||
->columnName('device_brand')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('deviceModel', 'string')
|
||||
->columnName('device_model')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('trackingId', 'string')
|
||||
->columnName('tracking_id')
|
||||
->unique()
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadDevice')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'lead',
|
||||
'clientInfo',
|
||||
'device',
|
||||
'deviceBrand',
|
||||
'deviceModel',
|
||||
'deviceOsName',
|
||||
'deviceOsShortName',
|
||||
'deviceOsVersion',
|
||||
'deviceOsPlatform',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
public function getSignature(): string
|
||||
{
|
||||
return md5(json_encode($this->clientInfo).$this->device.$this->deviceOsName.$this->deviceOsPlatform.$this->deviceBrand.$this->deviceModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getClientInfo()
|
||||
{
|
||||
return $this->clientInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $clientInfo
|
||||
*/
|
||||
public function setClientInfo($clientInfo): void
|
||||
{
|
||||
$this->clientInfo = $clientInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDevice()
|
||||
{
|
||||
return $this->device;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $device
|
||||
*/
|
||||
public function setDevice($device): void
|
||||
{
|
||||
$this->device = $device;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDeviceBrand()
|
||||
{
|
||||
return $this->deviceBrand;
|
||||
}
|
||||
|
||||
public function setDeviceBrand($brand): void
|
||||
{
|
||||
$this->deviceBrand = $brand;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDeviceModel()
|
||||
{
|
||||
return $this->deviceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $deviceModel
|
||||
*/
|
||||
public function setDeviceModel($deviceModel): void
|
||||
{
|
||||
$this->deviceModel = $deviceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsName()
|
||||
{
|
||||
return $this->deviceOsName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsName
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsName($deviceOsName)
|
||||
{
|
||||
$this->deviceOsName = $deviceOsName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsShortName()
|
||||
{
|
||||
return $this->deviceOsShortName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsShortName
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsShortName($deviceOsShortName)
|
||||
{
|
||||
$this->deviceOsShortName = $deviceOsShortName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsVersion()
|
||||
{
|
||||
return $this->deviceOsVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsVersion
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsVersion($deviceOsVersion)
|
||||
{
|
||||
$this->deviceOsVersion = $deviceOsVersion;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOsPlatform()
|
||||
{
|
||||
return $this->deviceOsPlatform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $deviceOsPlatform
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeviceOsPlatform($deviceOsPlatform)
|
||||
{
|
||||
$this->deviceOsPlatform = $deviceOsPlatform;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDeviceOs()
|
||||
{
|
||||
return $this->deviceOsName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $deviceOs
|
||||
*/
|
||||
public function setDeviceOs($deviceOs): void
|
||||
{
|
||||
if (isset($deviceOs['name'])) {
|
||||
$this->deviceOsName = $deviceOs['name'];
|
||||
}
|
||||
if (isset($deviceOs['short_name'])) {
|
||||
$this->deviceOsShortName = $deviceOs['short_name'];
|
||||
}
|
||||
if (isset($deviceOs['version'])) {
|
||||
$this->deviceOsVersion = $deviceOs['version'];
|
||||
}
|
||||
if (isset($deviceOs['platform'])) {
|
||||
$this->deviceOsPlatform = $deviceOs['platform'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTrackingId()
|
||||
{
|
||||
return $this->trackingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $trackingId
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setTrackingId($trackingId)
|
||||
{
|
||||
$this->trackingId = $trackingId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $dateAdded
|
||||
*/
|
||||
public function setDateAdded($dateAdded): void
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadDevice>
|
||||
*/
|
||||
class LeadDeviceRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* {@inhertidoc}.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder($this->getTableAlias())
|
||||
->select($this->getTableAlias());
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'd';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDevice($lead, $deviceNames = null, $deviceBrands = null, $deviceModels = null, $deviceOss = null, $deviceId = null)
|
||||
{
|
||||
$selectQuery = $this->_em->getConnection()->createQueryBuilder();
|
||||
$selectQuery->select('es.id as id, es.device as device')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_devices', 'es');
|
||||
|
||||
if (null !== $deviceNames) {
|
||||
if (!is_array($deviceNames)) {
|
||||
$deviceNames = [$deviceNames];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceName) => $selectQuery->expr()->eq('es.device', ':device'.$key), array_keys($deviceNames), $deviceNames)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceNames as $key => $deviceName) {
|
||||
$selectQuery->setParameter('device'.$key, $deviceName);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceBrands) {
|
||||
if (!is_array($deviceBrands)) {
|
||||
$deviceBrands = [$deviceBrands];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceBrand) => $selectQuery->expr()->eq('es.device_brand', ':deviceBrand'.$key), array_keys($deviceBrands), $deviceBrands)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceBrands as $key => $deviceBrand) {
|
||||
$selectQuery->setParameter('deviceBrand'.$key, $deviceBrand);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceModels) {
|
||||
if (!is_array($deviceModels)) {
|
||||
$deviceModels = [$deviceModels];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceModel) => $selectQuery->expr()->eq('es.device_model', ':deviceModel'.$key), array_keys($deviceModels), $deviceModels)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceModels as $key => $deviceModel) {
|
||||
$selectQuery->setParameter('deviceModel'.$key, $deviceModel);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceOss) {
|
||||
if (!is_array($deviceOss)) {
|
||||
$deviceOss = [$deviceOss];
|
||||
}
|
||||
|
||||
$or = $selectQuery->expr()->or(
|
||||
...array_map(fn ($key, $deviceOs) => $selectQuery->expr()->eq('es.device_os_name', ':deviceOs'.$key), array_keys($deviceOss), $deviceOss)
|
||||
);
|
||||
$selectQuery->andWhere($or);
|
||||
foreach ($deviceOss as $key => $deviceOs) {
|
||||
$selectQuery->setParameter('deviceOs'.$key, $deviceOs);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $deviceId) {
|
||||
$selectQuery->andWhere(
|
||||
$selectQuery->expr()->eq('es.id', $deviceId)
|
||||
);
|
||||
} elseif (null !== $lead) {
|
||||
$selectQuery->andWhere(
|
||||
$selectQuery->expr()->eq('es.lead_id', $lead->getId())
|
||||
);
|
||||
}
|
||||
|
||||
// get totals
|
||||
$device = $selectQuery->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return (!empty($device)) ? $device[0] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $trackingId
|
||||
*
|
||||
* @return LeadDevice|null
|
||||
*/
|
||||
public function getByTrackingId($trackingId)
|
||||
{
|
||||
/** @var LeadDevice $leadDevice */
|
||||
$leadDevice = $this->findOneBy([
|
||||
'trackingId' => $trackingId,
|
||||
]);
|
||||
|
||||
return $leadDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is at least one device with filled tracking code assigned to Lead.
|
||||
*/
|
||||
public function isAnyLeadDeviceTracked(Lead $lead): bool
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$qb = $this->createQueryBuilder($alias);
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq($alias.'.lead', ':lead'),
|
||||
$qb->expr()->isNotNull($alias.'.trackingId')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', $lead);
|
||||
|
||||
$devices = $qb->getQuery()->getResult();
|
||||
|
||||
return !empty($devices);
|
||||
}
|
||||
|
||||
public function getLeadDevices(Lead $lead): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
return $qb->select('*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_devices', 'es')
|
||||
->where('lead_id = :leadId')
|
||||
->setParameter('leadId', (int) $lead->getId())
|
||||
->orderBy('date_added', 'desc')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_devices')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function findExistingDevice(LeadDevice $device): ?LeadDevice
|
||||
{
|
||||
return $this->findOneBy(
|
||||
[
|
||||
'lead' => $device->getLead(),
|
||||
'device' => $device->getDevice(),
|
||||
'deviceBrand' => $device->getDeviceBrand(),
|
||||
'deviceModel' => $device->getDeviceModel(),
|
||||
'deviceOsName' => $device->getDeviceOsName(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
/**
|
||||
* Store here contact events.
|
||||
*/
|
||||
class LeadEventLog
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const INDEX_SEARCH = 'IDX_SEARCH';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
protected $lead;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
protected $userId;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $userName;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $bundle;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
protected $objectId;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
protected $dateAdded;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
private $properties = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDateAdded(new \DateTime());
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->setTable('lead_event_log')
|
||||
->setCustomRepositoryClass(LeadEventLogRepository::class)
|
||||
->addIndex(['lead_id'], 'lead_id_index')
|
||||
->addIndex(['object', 'object_id'], 'lead_object_index')
|
||||
->addIndex(['bundle', 'object', 'action', 'object_id'], 'lead_timeline_index')
|
||||
->addIndex(['bundle', 'object', 'action', 'object_id', 'date_added'], self::INDEX_SEARCH)
|
||||
->addIndex(['action'], 'lead_timeline_action_index')
|
||||
->addIndex(['date_added'], 'lead_date_added_index')
|
||||
->addBigIntIdField()
|
||||
->addNullableField('userId', Types::INTEGER, 'user_id')
|
||||
->addNullableField('userName', Types::STRING, 'user_name')
|
||||
->addNullableField('bundle', Types::STRING)
|
||||
->addNullableField('object', Types::STRING)
|
||||
->addNullableField('action', Types::STRING)
|
||||
->addNullableField('objectId', Types::INTEGER, 'object_id')
|
||||
->addNamedField('dateAdded', Types::DATETIME_MUTABLE, 'date_added')
|
||||
->addNullableField('properties', Types::JSON);
|
||||
|
||||
$builder->createManyToOne('lead', Lead::class)
|
||||
->addJoinColumn('lead_id', 'id', true, false, 'CASCADE')
|
||||
->inversedBy('eventLog')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('import')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'leadId',
|
||||
'userId',
|
||||
'userName',
|
||||
'bundle',
|
||||
'object',
|
||||
'action',
|
||||
'objectId',
|
||||
'dateAdded',
|
||||
'properties',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lead.
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lead.
|
||||
*
|
||||
* @return Lead|null
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userId.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setUserId($userId)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userId.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUserId()
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set object.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setObject($object)
|
||||
{
|
||||
$this->object = $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set objectId.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setObjectId($objectId)
|
||||
{
|
||||
$this->objectId = $objectId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get objectId.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getObjectId()
|
||||
{
|
||||
return $this->objectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set action.
|
||||
*
|
||||
* @param string $action
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setAction($action)
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAction()
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set properties.
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setProperties(array $properties)
|
||||
{
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set one property into the properties array.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function addProperty($key, $value)
|
||||
{
|
||||
$this->properties[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateAdded.
|
||||
*
|
||||
* @param \DateTime $dateAdded
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateAdded.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bundle.
|
||||
*
|
||||
* @param string $bundle
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setBundle($bundle)
|
||||
{
|
||||
$this->bundle = $bundle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bundle.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBundle()
|
||||
{
|
||||
return $this->bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userName.
|
||||
*
|
||||
* @param string $userName
|
||||
*
|
||||
* @return LeadEventLog
|
||||
*/
|
||||
public function setUserName($userName)
|
||||
{
|
||||
$this->userName = $userName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName()
|
||||
{
|
||||
return $this->userName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadEventLog>
|
||||
*/
|
||||
class LeadEventLogRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Returns paginator with failed rows.
|
||||
*
|
||||
* @param string $bundle
|
||||
* @param string $object
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getFailedRows($importId, array $args = [], $bundle = 'lead', $object = 'import')
|
||||
{
|
||||
return $this->getSpecificRows($importId, 'failed', $args, $bundle, $object);
|
||||
}
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$entities = parent::getEntities($args);
|
||||
$entities = iterator_to_array($entities);
|
||||
|
||||
foreach ($entities as $key => $row) {
|
||||
if (
|
||||
isset($row['properties']['error'])
|
||||
&& preg_match('/SQLSTATE\[\w+\]: (.*)/', $row['properties']['error'], $matches)
|
||||
) {
|
||||
$entities[$key]['properties']['error'] = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns paginator with specific type of rows.
|
||||
*
|
||||
* @param string $bundle
|
||||
* @param string $object
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getSpecificRows($objectId, $action, array $args = [], $bundle = 'lead', $object = 'import')
|
||||
{
|
||||
return $this->getEntities(
|
||||
array_merge(
|
||||
[
|
||||
'start' => 0,
|
||||
'limit' => 100,
|
||||
'orderBy' => $this->getTableAlias().'.dateAdded',
|
||||
'orderByDir' => 'ASC',
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => $this->getTableAlias().'.bundle',
|
||||
'expr' => 'eq',
|
||||
'value' => $bundle,
|
||||
],
|
||||
[
|
||||
'column' => $this->getTableAlias().'.object',
|
||||
'expr' => 'eq',
|
||||
'value' => $object,
|
||||
],
|
||||
[
|
||||
'column' => $this->getTableAlias().'.action',
|
||||
'expr' => 'eq',
|
||||
'value' => $action,
|
||||
],
|
||||
[
|
||||
'column' => $this->getTableAlias().'.objectId',
|
||||
'expr' => 'eq',
|
||||
'value' => $objectId,
|
||||
],
|
||||
],
|
||||
],
|
||||
'hydration_mode' => 'HYDRATE_ARRAY',
|
||||
],
|
||||
$args
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $bundle
|
||||
* @param ?string $object
|
||||
* @param array|string|null $actions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEvents(?Lead $contact = null, $bundle = null, $object = null, $actions = null, array $options = [])
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_event_log', $alias);
|
||||
|
||||
if ($contact) {
|
||||
$qb->andWhere($alias.'.lead_id = :lead')
|
||||
->setParameter('lead', $contact->getId());
|
||||
}
|
||||
|
||||
if ($bundle) {
|
||||
$qb->andWhere($alias.'.bundle = :bundle')
|
||||
->setParameter('bundle', $bundle);
|
||||
}
|
||||
|
||||
if ($object) {
|
||||
$qb->andWhere($alias.'.object = :object')
|
||||
->setParameter('object', $object);
|
||||
}
|
||||
|
||||
if ($actions) {
|
||||
if (is_array($actions)) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->in($alias.'.action', ':actions')
|
||||
)
|
||||
->setParameter('actions', $actions, ArrayParameterType::STRING);
|
||||
} else {
|
||||
$qb->andWhere($alias.'.action = :action')
|
||||
->setParameter('action', $actions);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($options['search'])) {
|
||||
$qb->andWhere($qb->expr()->like('LOWER('.$alias.'.properties)', $qb->expr()->literal('%'.strtolower($options['search']).'%')));
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($qb, $options, $alias.'.action', $alias.'.date_added', [], ['date_added'], null, $alias.'.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*
|
||||
* @param int $fromLeadId
|
||||
* @param int $toLeadId
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_event_log')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines default table alias for lead_event_log table.
|
||||
*/
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'lel';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,934 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CacheInvalidateInterface;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\LeadBundle\Field\DTO\CustomFieldObject;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\FieldAliasKeyword;
|
||||
use Mautic\LeadBundle\Validator\LeadFieldMinimumLength;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('lead:leads:viewown')"),
|
||||
new Post(security: "is_granted('lead:leads:create')"),
|
||||
new Get(security: "is_granted('lead:leads:viewown')"),
|
||||
new Put(security: "is_granted('lead:leads:editown')"),
|
||||
new Patch(security: "is_granted('lead:leads:editother')"),
|
||||
new Delete(security: "is_granted('lead:leads:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['leadfield:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['leadfield:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class LeadField extends FormEntity implements CacheInvalidateInterface, UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
public const MAX_VARCHAR_LENGTH = 191;
|
||||
public const CACHE_NAMESPACE = 'LeadField';
|
||||
public const TYPES_SUPPORTING_LENGTH = [
|
||||
'text',
|
||||
'select',
|
||||
'phone',
|
||||
'url',
|
||||
'email',
|
||||
];
|
||||
public const ENTITY_NAME = 'lead_field';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private $id;
|
||||
|
||||
private bool $isCloned = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $label;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $alias;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $type = 'text';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $group = 'core';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $defaultValue;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isRequired = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isFixed = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isVisible = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isShortVisible = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isListable = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isPubliclyUpdatable = false;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isUniqueIdentifer = false;
|
||||
|
||||
/**
|
||||
* Workaround for incorrectly spelled $isUniqueIdentifer.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $isUniqueIdentifier = false;
|
||||
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private ?int $charLengthLimit = 64;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $order = 1;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $object = 'lead';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private $properties = [];
|
||||
|
||||
#[Groups(['leadfield:read', 'leadfield:write'])]
|
||||
private bool $isIndex = false;
|
||||
|
||||
/**
|
||||
* The column in lead_fields table was not created yet if this property is true.
|
||||
* Entity cannot be published and we cannot work with it until column is created.
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private bool $columnIsNotCreated = false;
|
||||
|
||||
/**
|
||||
* The column in lead_fields table was not removed yet if this property is true.
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private bool $columnIsNotRemoved = false;
|
||||
|
||||
/**
|
||||
* This property contains an original value for $isPublished.
|
||||
* $isPublished is always set on false if $columnIsNotCreated is true.
|
||||
*/
|
||||
#[Groups(['leadfield:read'])]
|
||||
private bool $originalIsPublishedValue = false;
|
||||
|
||||
/**
|
||||
* @var CustomFieldObject
|
||||
*/
|
||||
private $customFieldObject;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
$this->isCloned = true;
|
||||
$this->order = 0;
|
||||
$this->isFixed = false;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->addLifecycleEvent('identifierWorkaround', 'postLoad');
|
||||
|
||||
$builder->setTable('lead_fields')
|
||||
->setCustomRepositoryClass(LeadFieldRepository::class)
|
||||
->addIndex(['object', 'field_order', 'is_published'], 'idx_object_field_order_is_published');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addField('label', 'string');
|
||||
|
||||
$builder->addField('alias', 'string');
|
||||
|
||||
$builder->createField('type', 'string')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createField('group', 'string')
|
||||
->columnName('field_group')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('defaultValue', 'string')
|
||||
->columnName('default_value')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('isRequired', 'boolean')
|
||||
->columnName('is_required')
|
||||
->build();
|
||||
|
||||
$builder->createField('isFixed', 'boolean')
|
||||
->columnName('is_fixed')
|
||||
->build();
|
||||
|
||||
$builder->createField('isVisible', 'boolean')
|
||||
->columnName('is_visible')
|
||||
->build();
|
||||
|
||||
$builder->createField('isShortVisible', 'boolean')
|
||||
->columnName('is_short_visible')
|
||||
->nullable(false)
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('isListable', 'boolean')
|
||||
->columnName('is_listable')
|
||||
->build();
|
||||
|
||||
$builder->createField('isPubliclyUpdatable', 'boolean')
|
||||
->columnName('is_publicly_updatable')
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('isUniqueIdentifer', 'boolean', 'is_unique_identifer');
|
||||
|
||||
$builder->createField('isIndex', 'boolean')
|
||||
->columnName('is_index')
|
||||
->option('default', false)
|
||||
->nullable(false)
|
||||
->build();
|
||||
|
||||
$builder->createField('charLengthLimit', 'integer')
|
||||
->columnName('char_length_limit')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('order', 'integer')
|
||||
->columnName('field_order')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('object', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('properties', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('columnIsNotCreated', 'boolean')
|
||||
->columnName('column_is_not_created')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('columnIsNotRemoved', Types::BOOLEAN)
|
||||
->columnName('column_is_not_removed')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('originalIsPublishedValue', 'boolean')
|
||||
->columnName('original_is_published_value')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('label', new Assert\NotBlank(
|
||||
['message' => 'mautic.lead.field.label.notblank']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('label', new Assert\Length([
|
||||
'max' => 191,
|
||||
'maxMessage' => 'mautic.lead.field.label.maxlength',
|
||||
]));
|
||||
|
||||
$metadata->addConstraint(new UniqueEntity([
|
||||
'fields' => ['alias'],
|
||||
'message' => 'mautic.lead.field.alias.unique',
|
||||
]));
|
||||
|
||||
$metadata->addConstraint(new Assert\Callback(
|
||||
function (LeadField $field, ExecutionContextInterface $context): void {
|
||||
$violations = $context->getValidator()->validate($field, [new FieldAliasKeyword()]);
|
||||
|
||||
if ($violations->count() > 0) {
|
||||
$context->buildViolation($violations->get(0)->getMessage())
|
||||
->atPath('alias')
|
||||
->addViolation();
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new LeadFieldMinimumLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadField')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'label',
|
||||
'alias',
|
||||
'type',
|
||||
'group',
|
||||
'order',
|
||||
'object',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'defaultValue',
|
||||
'isRequired',
|
||||
'isFixed',
|
||||
'isListable',
|
||||
'isVisible',
|
||||
'isVisible',
|
||||
'isShortVisible',
|
||||
'isUniqueIdentifier',
|
||||
'isPubliclyUpdatable',
|
||||
'properties',
|
||||
'isIndex',
|
||||
'charLengthLimit',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function setId(?int $id = null): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getIsCloned(): bool
|
||||
{
|
||||
return $this->isCloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set label.
|
||||
*
|
||||
* @param string $label
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setLabel($label)
|
||||
{
|
||||
$this->isChanged('label', $label);
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get label.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLabel()
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy function to setLabel().
|
||||
*
|
||||
* @param string $label
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setName($label)
|
||||
{
|
||||
$this->isChanged('label', $label);
|
||||
|
||||
return $this->setLabel($label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy function for getLabel().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->isChanged('type', $type);
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set defaultValue.
|
||||
*
|
||||
* @param string|array<string> $defaultValue
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setDefaultValue($defaultValue)
|
||||
{
|
||||
$defaultValue = is_array($defaultValue) ? implode('|', $defaultValue) : $defaultValue;
|
||||
$this->isChanged('defaultValue', $defaultValue);
|
||||
$this->defaultValue = $defaultValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get defaultValue.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDefaultValue()
|
||||
{
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isRequired.
|
||||
*
|
||||
* @param bool $isRequired
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsRequired($isRequired)
|
||||
{
|
||||
$this->isChanged('isRequired', $isRequired);
|
||||
$this->isRequired = $isRequired;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isRequired.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsRequired()
|
||||
{
|
||||
return $this->isRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsRequired().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRequired()
|
||||
{
|
||||
return $this->getIsRequired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isFixed.
|
||||
*
|
||||
* @param bool $isFixed
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsFixed($isFixed)
|
||||
{
|
||||
$this->isFixed = $isFixed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isFixed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsFixed()
|
||||
{
|
||||
return $this->isFixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsFixed().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFixed()
|
||||
{
|
||||
return $this->getIsFixed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $properties
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setProperties($properties)
|
||||
{
|
||||
$this->isChanged('properties', $properties);
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set order.
|
||||
*
|
||||
* @param int $order
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setOrder($order)
|
||||
{
|
||||
$this->isChanged('order', $order);
|
||||
$this->order = $order;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
public function setCharLengthLimit(?int $charLengthLimit): LeadField
|
||||
{
|
||||
$this->isChanged('charLengthLimit', $charLengthLimit);
|
||||
$this->charLengthLimit = $charLengthLimit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCharLengthLimit(): ?int
|
||||
{
|
||||
return $this->charLengthLimit;
|
||||
}
|
||||
|
||||
public function getCustomFieldObject(): string
|
||||
{
|
||||
if (!$this->customFieldObject) {
|
||||
$this->customFieldObject = new CustomFieldObject($this);
|
||||
}
|
||||
|
||||
return $this->customFieldObject->getObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set object.
|
||||
*
|
||||
* @param string $object
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setObject($object)
|
||||
{
|
||||
$this->isChanged('object', $object);
|
||||
$this->object = $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isVisible.
|
||||
*
|
||||
* @param bool $isVisible
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsVisible($isVisible)
|
||||
{
|
||||
$this->isChanged('isVisible', $isVisible);
|
||||
$this->isVisible = $isVisible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isVisible.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsVisible()
|
||||
{
|
||||
return $this->isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsVisible().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible()
|
||||
{
|
||||
return $this->getIsVisible();
|
||||
}
|
||||
|
||||
public function setIsShortVisible(?bool $isShortVisible): self
|
||||
{
|
||||
$isShortVisible = $isShortVisible ?? false;
|
||||
$this->isChanged('isShortVisible', $isShortVisible);
|
||||
$this->isShortVisible = $isShortVisible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isShortVisible.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsShortVisible()
|
||||
{
|
||||
return $this->isShortVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsShortVisible().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isShortVisible()
|
||||
{
|
||||
return $this->getIsShortVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique identifer state of the field.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsUniqueIdentifer()
|
||||
{
|
||||
return $this->isUniqueIdentifer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the unique identifer state of the field.
|
||||
*
|
||||
* @param mixed $isUniqueIdentifer
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsUniqueIdentifer($isUniqueIdentifer)
|
||||
{
|
||||
if ($isUniqueIdentifer) {
|
||||
$this->isIndex = true;
|
||||
}
|
||||
|
||||
$this->isUniqueIdentifer = $this->isUniqueIdentifier = $isUniqueIdentifer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for incorrectly spelled setIsUniqueIdentifer.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsUniqueIdentifier()
|
||||
{
|
||||
return $this->getIsUniqueIdentifer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for incorrectly spelled setIsUniqueIdentifer.
|
||||
*
|
||||
* @param mixed $isUniqueIdentifier
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsUniqueIdentifier($isUniqueIdentifier)
|
||||
{
|
||||
return $this->setIsUniqueIdentifer($isUniqueIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set alias.
|
||||
*
|
||||
* @param string $alias
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setAlias($alias)
|
||||
{
|
||||
$this->isChanged('alias', $alias);
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alias.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlias()
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isListable.
|
||||
*
|
||||
* @param bool $isListable
|
||||
*
|
||||
* @return LeadField
|
||||
*/
|
||||
public function setIsListable($isListable)
|
||||
{
|
||||
$this->isChanged('isListable', $isListable);
|
||||
$this->isListable = $isListable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isListable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsListable()
|
||||
{
|
||||
return $this->isListable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to getIsListable().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isListable()
|
||||
{
|
||||
return $this->getIsListable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGroup()
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $group
|
||||
*/
|
||||
public function setGroup($group): void
|
||||
{
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getIsPubliclyUpdatable()
|
||||
{
|
||||
return $this->isPubliclyUpdatable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $isPubliclyUpdatable
|
||||
*/
|
||||
public function setIsPubliclyUpdatable($isPubliclyUpdatable): void
|
||||
{
|
||||
$this->isPubliclyUpdatable = (bool) $isPubliclyUpdatable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for mispelled isUniqueIdentifer.
|
||||
*/
|
||||
public function identifierWorkaround(): void
|
||||
{
|
||||
$this->isUniqueIdentifier = $this->isUniqueIdentifer;
|
||||
}
|
||||
|
||||
public function isNew(): bool
|
||||
{
|
||||
return $this->getId() ? false : true;
|
||||
}
|
||||
|
||||
public function getColumnIsNotCreated(): bool
|
||||
{
|
||||
return $this->columnIsNotCreated;
|
||||
}
|
||||
|
||||
public function getColumnIsNotRemoved(): bool
|
||||
{
|
||||
return $this->columnIsNotRemoved;
|
||||
}
|
||||
|
||||
public function setColumnIsNotCreated(): void
|
||||
{
|
||||
$this->columnIsNotCreated = true;
|
||||
$this->originalIsPublishedValue = $this->getIsPublished();
|
||||
$this->setIsPublished(false);
|
||||
}
|
||||
|
||||
public function setColumnIsNotRemoved(): void
|
||||
{
|
||||
$this->columnIsNotRemoved = true;
|
||||
$this->setIsPublished(false);
|
||||
}
|
||||
|
||||
public function setColumnWasCreated(): void
|
||||
{
|
||||
$this->columnIsNotCreated = false;
|
||||
$this->setIsPublished($this->getOriginalIsPublishedValue());
|
||||
}
|
||||
|
||||
public function disablePublishChange(): bool
|
||||
{
|
||||
return 'email' === $this->getAlias() || $this->getColumnIsNotCreated() || $this->getColumnIsNotRemoved();
|
||||
}
|
||||
|
||||
public function getOriginalIsPublishedValue(): bool
|
||||
{
|
||||
return (bool) $this->originalIsPublishedValue;
|
||||
}
|
||||
|
||||
public function getCacheNamespacesToDelete(): array
|
||||
{
|
||||
return [self::CACHE_NAMESPACE];
|
||||
}
|
||||
|
||||
public function isIsIndex(): bool
|
||||
{
|
||||
return $this->isIndex;
|
||||
}
|
||||
|
||||
public function setIsIndex(?bool $indexable): void
|
||||
{
|
||||
$this->isIndex = $indexable ?? false;
|
||||
}
|
||||
|
||||
public function supportsLength(): bool
|
||||
{
|
||||
return in_array($this->type, self::TYPES_SUPPORTING_LENGTH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadField>
|
||||
*/
|
||||
class LeadFieldRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @var array<int|string, array<string,mixed>>|null
|
||||
*/
|
||||
private ?array $fields = null;
|
||||
|
||||
/**
|
||||
* Retrieves array of aliases used to ensure unique alias for new fields.
|
||||
*
|
||||
* @param int $exludingId
|
||||
* @param bool $publishedOnly
|
||||
* @param bool $includeEntityFields
|
||||
* @param string $object name of object using the custom fields
|
||||
*/
|
||||
public function getAliases($exludingId, $publishedOnly = false, $includeEntityFields = true, $object = 'lead'): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('l.alias')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'l');
|
||||
|
||||
if (!empty($exludingId)) {
|
||||
$q->where('l.id != :id')
|
||||
->setParameter('id', $exludingId);
|
||||
}
|
||||
|
||||
if ($publishedOnly) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('is_published', ':true')
|
||||
)
|
||||
->setParameter('true', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($object) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('l.object', ':object')
|
||||
)->setParameter('object', $object);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$aliases = [];
|
||||
foreach ($results as $item) {
|
||||
$aliases[] = $item['alias'];
|
||||
}
|
||||
|
||||
if ($includeEntityFields) {
|
||||
// add lead main column names to prevent attempt to create a field with the same name
|
||||
$leadRepo = $this->_em->getRepository(Lead::class)->getBaseColumns(Lead::class, true);
|
||||
$aliases = array_merge($aliases, $leadRepo);
|
||||
}
|
||||
|
||||
return $aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, array<string, mixed>>
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
if (!isset($this->fields)) {
|
||||
$fq = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$fq->select('f.id, f.label, f.alias, f.type, f.field_group as "group", f.object, f.is_fixed, f.properties, f.default_value')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'f')
|
||||
->where('f.is_published = :published')
|
||||
->setParameter('published', true, 'boolean')
|
||||
->addOrderBy('f.field_order', 'asc');
|
||||
$results = $fq->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$this->fields = array_column($results, null, 'alias');
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadField[]
|
||||
*/
|
||||
public function getFieldsForObject(string $object): array
|
||||
{
|
||||
$queryBuilder = $this->_em->createQueryBuilder();
|
||||
$queryBuilder->select($this->getTableAlias());
|
||||
$queryBuilder->from($this->getEntityName(), $this->getTableAlias(), "{$this->getTableAlias()}.id");
|
||||
$queryBuilder->where("{$this->getTableAlias()}.object = :object");
|
||||
$queryBuilder->andWhere("{$this->getTableAlias()}.isPublished = 1");
|
||||
$queryBuilder->orderBy("{$this->getTableAlias()}.label");
|
||||
$queryBuilder->setParameter('object', $object);
|
||||
|
||||
return $queryBuilder->getQuery()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the aliases of searchable fields that are indexed and published.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchableFieldAliases(?string $object = null): array
|
||||
{
|
||||
$fq = $this->createQueryBuilder($this->getTableAlias());
|
||||
$fq->select($this->getTableAlias().'.alias')
|
||||
->andWhere($fq->expr()->eq($this->getTableAlias().'.isIndex', true))
|
||||
->andWhere($fq->expr()->eq($this->getTableAlias().'.isPublished', true));
|
||||
|
||||
if (!empty($object)) {
|
||||
$fq->andWhere($fq->expr()->eq($this->getTableAlias().'.object', ':object'))
|
||||
->setParameter('object', $object, ParameterType::STRING);
|
||||
}
|
||||
|
||||
$results = $fq->getQuery()->getResult();
|
||||
|
||||
return array_column($results, 'alias');
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'f';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
* @param object $filter
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'f.label',
|
||||
'f.alias',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['f.order', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field aliases for lead table columns.
|
||||
*
|
||||
* @param string $object name of object using the custom fields
|
||||
*/
|
||||
public function getFieldAliases($object = 'lead'): array
|
||||
{
|
||||
$qb = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
return $qb->select('f.alias, f.is_unique_identifer as is_unique, f.type, f.object')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields', 'f')
|
||||
->where($qb->expr()->eq('object', ':object'))
|
||||
->setParameter('object', $object)
|
||||
->orderBy('f.field_order', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection<int,LeadField>
|
||||
*/
|
||||
public function getListablePublishedFields(): ArrayCollection
|
||||
{
|
||||
$queryBuilder = $this->_em->createQueryBuilder();
|
||||
$queryBuilder->select($this->getTableAlias());
|
||||
$queryBuilder->from($this->getEntityName(), $this->getTableAlias(), "{$this->getTableAlias()}.id");
|
||||
$queryBuilder->where("{$this->getTableAlias()}.isListable = 1");
|
||||
$queryBuilder->andWhere("{$this->getTableAlias()}.isPublished = 1");
|
||||
$queryBuilder->orderBy("{$this->getTableAlias()}.object");
|
||||
|
||||
return new ArrayCollection($queryBuilder->getQuery()->execute());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add company left join.
|
||||
*
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
private function addCompanyLeftJoin($q): void
|
||||
{
|
||||
$q->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'companies_lead', 'l.id = companies_lead.lead_id');
|
||||
$q->leftJoin('companies_lead', MAUTIC_TABLE_PREFIX.'companies', 'company', 'companies_lead.company_id = company.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return property by field alias and join tables.
|
||||
*
|
||||
* @param string $field
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
public function getPropertyByField($field, $q): string
|
||||
{
|
||||
$columnAlias = 'l.';
|
||||
// Join company tables If we're trying search by company fields
|
||||
if (in_array($field, array_column($this->getFieldAliases('company'), 'alias'))) {
|
||||
$this->addCompanyLeftJoin($q);
|
||||
$columnAlias = 'company.';
|
||||
} elseif (in_array($field, ['utm_campaign', 'utm_content', 'utm_medium', 'utm_source', 'utm_term'])) {
|
||||
$q->join('l', MAUTIC_TABLE_PREFIX.'lead_utmtags', 'u', 'l.id = u.lead_id');
|
||||
$columnAlias = 'u.';
|
||||
}
|
||||
|
||||
return $columnAlias.$field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with defined value for defined lead.
|
||||
*
|
||||
* @param int $lead ID
|
||||
* @param string $field alias
|
||||
* @param mixed $value to compare with
|
||||
* @param string $operatorExpr for WHERE clause
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function compareValue($lead, $field, $value, $operatorExpr, ?string $fieldType = null)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
|
||||
|
||||
if ('tags' === $field) {
|
||||
// Special reserved tags field
|
||||
$q->join('l', MAUTIC_TABLE_PREFIX.'lead_tags_xref', 'x', 'l.id = x.lead_id')
|
||||
->join('x', MAUTIC_TABLE_PREFIX.'lead_tags', 't', 'x.tag_id = t.id')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->eq('t.tag', ':value')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
if (('eq' === $operatorExpr) || ('like' === $operatorExpr)) {
|
||||
return !empty($result['id']);
|
||||
} elseif (('neq' === $operatorExpr) || ('notLike' === $operatorExpr)) {
|
||||
return empty($result['id']);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$property = $this->getPropertyByField($field, $q);
|
||||
if ('empty' === $operatorExpr || 'notEmpty' === $operatorExpr) {
|
||||
$doesSupportEmptyValue = !in_array($fieldType, ['date', 'datetime'], true);
|
||||
$compositeExpression = ('empty' === $operatorExpr) ?
|
||||
$q->expr()->or(
|
||||
$q->expr()->isNull($property),
|
||||
$doesSupportEmptyValue ? $q->expr()->eq($property, $q->expr()->literal('')) : null
|
||||
)
|
||||
:
|
||||
$q->expr()->and(
|
||||
$q->expr()->isNotNull($property),
|
||||
$doesSupportEmptyValue ? $q->expr()->neq($property, $q->expr()->literal('')) : null
|
||||
);
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$compositeExpression
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead);
|
||||
} elseif ('regexp' === $operatorExpr || 'notRegexp' === $operatorExpr) {
|
||||
if ('regexp' === $operatorExpr) {
|
||||
$where = $property.' REGEXP :value';
|
||||
} else {
|
||||
$where = $property.' NOT REGEXP :value';
|
||||
}
|
||||
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->and($where)
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
} elseif ('in' === $operatorExpr || 'notIn' === $operatorExpr) {
|
||||
$values = (!is_array($value)) ? [$value] : $value;
|
||||
$operator = str_starts_with($operatorExpr, 'not') ? 'NOT REGEXP' : 'REGEXP';
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead')
|
||||
);
|
||||
|
||||
$innerExpr = [];
|
||||
foreach ($values as $v) {
|
||||
$v = $q->expr()->literal(
|
||||
InputHelper::clean($v)
|
||||
);
|
||||
|
||||
$v = trim($v, "'");
|
||||
$innerExpr[] = $property." $operator '\\\\|?$v\\\\|?'";
|
||||
}
|
||||
|
||||
if (str_starts_with($operatorExpr, 'not')) {
|
||||
$expr = $expr->with($q->expr()->or(
|
||||
$q->expr()->isNull($property),
|
||||
$q->expr()->and(...$innerExpr)
|
||||
));
|
||||
} else {
|
||||
$expr = $expr->with($q->expr()->or(...$innerExpr));
|
||||
}
|
||||
|
||||
$q->where($expr)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('values', $values, ArrayParameterType::STRING);
|
||||
} else {
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead')
|
||||
);
|
||||
|
||||
if ('neq' === $operatorExpr) {
|
||||
// include null
|
||||
$expr = $expr->with(
|
||||
$q->expr()->or(
|
||||
$q->expr()->$operatorExpr($property, ':value'),
|
||||
$q->expr()->isNull($property)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
switch ($operatorExpr) {
|
||||
case 'startsWith':
|
||||
$operatorExpr = 'like';
|
||||
$value = $value.'%';
|
||||
break;
|
||||
case 'endsWith':
|
||||
$operatorExpr = 'like';
|
||||
$value = '%'.$value;
|
||||
break;
|
||||
case 'contains':
|
||||
$operatorExpr = 'like';
|
||||
$value = '%'.$value.'%';
|
||||
break;
|
||||
}
|
||||
|
||||
$expr = $expr->with(
|
||||
$q->expr()->$operatorExpr($property, ':value')
|
||||
);
|
||||
}
|
||||
|
||||
$q->where($expr)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
}
|
||||
if (str_starts_with($property, 'u.')) {
|
||||
// Match only against the latest UTM properties.
|
||||
$q->orderBy('u.date_added', 'DESC');
|
||||
$q->setMaxResults(1);
|
||||
}
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with empty value for defined lead.
|
||||
*/
|
||||
public function compareEmptyDateValue(int $lead, string $field, string $operatorExpr): bool
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$property = $this->getPropertyByField($field, $q);
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
('empty' === $operatorExpr) ?
|
||||
$q->expr()->isNull($property)
|
||||
:
|
||||
$q->expr()->isNotNull($property)
|
||||
)
|
||||
)
|
||||
->setParameter('lead', $lead, \PDO::PARAM_INT);
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with defined date value for defined lead.
|
||||
*
|
||||
* @param int $lead ID
|
||||
* @param int $field alias
|
||||
* @param string $value to compare with
|
||||
*/
|
||||
public function compareDateValue($lead, $field, $value): bool
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$property = $this->getPropertyByField($field, $q);
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->eq($property, ':value')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('value', $value);
|
||||
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a form result value with defined date value ( only day and month compare for
|
||||
* events such as anniversary) for defined lead.
|
||||
*
|
||||
* @param int $lead ID
|
||||
* @param int $field alias
|
||||
* @param object $value Date object to compare with
|
||||
*/
|
||||
public function compareDateMonthValue($lead, $field, $value): bool
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('l.id', ':lead'),
|
||||
$q->expr()->eq("MONTH(l. $field)", ':month'),
|
||||
$q->expr()->eq("DAY(l. $field)", ':day')
|
||||
)
|
||||
)
|
||||
->setParameter('lead', (int) $lead)
|
||||
->setParameter('month', $value->format('m'))
|
||||
->setParameter('day', $value->format('d'));
|
||||
|
||||
$result = $q->executeQuery()->fetchAssociative();
|
||||
|
||||
return !empty($result['id']);
|
||||
}
|
||||
|
||||
public function getFieldThatIsMissingColumn(): ?LeadField
|
||||
{
|
||||
$qb = $this->createQueryBuilder($this->getTableAlias());
|
||||
$qb->where($qb->expr()->eq("{$this->getTableAlias()}.columnIsNotCreated", 1));
|
||||
$qb->orderBy("{$this->getTableAlias()}.dateAdded", Order::Ascending->value);
|
||||
$qb->setMaxResults(1);
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadField[]
|
||||
*/
|
||||
public function getFieldsByType($type)
|
||||
{
|
||||
return $this->findBy(['type' => $type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.lead.field.searchcommand.isindexed',
|
||||
'mautic.lead.field.searchcommand.isunique',
|
||||
'mautic.lead.field.searchcommand.type',
|
||||
'mautic.lead.field.searchcommand.group',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFieldSchemaData(string $object): array
|
||||
{
|
||||
return $this->_em->createQueryBuilder()
|
||||
->select('f.alias, f.label, f.type, f.isUniqueIdentifer, f.charLengthLimit')
|
||||
->from($this->getEntityName(), 'f', 'f.alias')
|
||||
->where('f.object = :object')
|
||||
->setParameter('object', $object)
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
* @param \StdClass $filter
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
list($expr, $parameters) = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
$prefix = $this->getTableAlias();
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.isindexed'):
|
||||
$expr = $q->expr()->eq($prefix.'.isIndex', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.isunique'):
|
||||
$expr = $q->expr()->eq($prefix.'.isUniqueIdentifer', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.type'):
|
||||
$forceParameters = [
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = $q->expr()->like($prefix.'.type', ":$unique");
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.field.searchcommand.group'):
|
||||
$forceParameters = [
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = $q->expr()->like($prefix.'.group', ":$unique");
|
||||
$returnParameter = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\SegmentInUse;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\UniqueUserAlias;
|
||||
use Mautic\LeadBundle\Validator\Constraints\SegmentUsedInCampaigns;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Segments',
|
||||
operations: [
|
||||
new GetCollection(uriTemplate: '/segments', security: "is_granted('lead:lists:viewown')"),
|
||||
new Post(uriTemplate: '/segments', security: "is_granted('lead:lists:create')"),
|
||||
new Get(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:viewown')"),
|
||||
new Put(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:editown')"),
|
||||
new Patch(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:editother')"),
|
||||
new Delete(uriTemplate: '/segments/{id}', security: "is_granted('lead:lists:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['segment:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['segment:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class LeadList extends FormEntity implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
use ProjectTrait;
|
||||
|
||||
public const TABLE_NAME = 'lead_lists';
|
||||
public const ENTITY_NAME = 'lists';
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $publicName;
|
||||
|
||||
/**
|
||||
* @var Category|null
|
||||
**/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $alias;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $filters = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $isGlobal = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['segment:read', 'segment:write', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $isPreferenceCenter = false;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<ListLead>
|
||||
*/
|
||||
private $leads;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $lastBuiltDate;
|
||||
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
#[Groups(['segment:read', 'campaign:read', 'email:read', 'sms:read'])]
|
||||
private $lastBuiltTime;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->leads = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(LeadListRepository::class)
|
||||
->addIndex(['alias'], 'lead_list_alias');
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->addField('alias', 'string');
|
||||
|
||||
$builder->createField('publicName', 'string')
|
||||
->columnName('public_name')
|
||||
->build();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->addField('filters', 'array');
|
||||
|
||||
$builder->createField('isGlobal', 'boolean')
|
||||
->columnName('is_global')
|
||||
->build();
|
||||
|
||||
$builder->createField('isPreferenceCenter', 'boolean')
|
||||
->columnName('is_preference_center')
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('leads', 'ListLead')
|
||||
->mappedBy('list')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createField('lastBuiltDate', 'datetime')
|
||||
->columnName('last_built_date')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('lastBuiltTime', 'float')
|
||||
->columnName('last_built_time')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
self::addProjectsField($builder, 'lead_list_projects_xref', 'leadlist_id');
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new Assert\NotBlank(
|
||||
['message' => 'mautic.core.name.required']
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new UniqueUserAlias([
|
||||
'field' => 'alias',
|
||||
'message' => 'mautic.lead.list.alias.unique',
|
||||
]));
|
||||
|
||||
$metadata->addConstraint(new SegmentUsedInCampaigns());
|
||||
$metadata->addConstraint(new SegmentInUse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadList')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'publicName',
|
||||
'alias',
|
||||
'description',
|
||||
'category',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'filters',
|
||||
'isGlobal',
|
||||
'isPreferenceCenter',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'leadList');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $description
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setCategory(?Category $category = null): LeadList
|
||||
{
|
||||
$this->isChanged('category', $category);
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategory(): ?Category
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publicName.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPublicName()
|
||||
{
|
||||
return $this->publicName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $publicName
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setPublicName($publicName)
|
||||
{
|
||||
$this->isChanged('publicName', $publicName);
|
||||
$this->publicName = $publicName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setFilters(array $filters)
|
||||
{
|
||||
$this->isChanged('filters', $filters);
|
||||
$this->filters = $filters;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
if (is_array($this->filters)) {
|
||||
return $this->setFirstFilterGlueToAnd($this->addLegacyParams($this->filters)); // @phpstan-ignore method.deprecated
|
||||
}
|
||||
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function needsRebuild(): bool
|
||||
{
|
||||
// Manual or unpublished segments never require rebuild
|
||||
if (empty($this->getFilters()) || !$this->isPublished()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A segment with filters requires rebuild if it was changed since the last build date, or was never built
|
||||
if (null === $this->getLastBuiltDate()) {
|
||||
return true;
|
||||
}
|
||||
if (null !== $this->getDateModified() && $this->getDateModified()->getTimestamp() >= $this->getLastBuiltDate()->getTimestamp()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasFilterTypeOf(string $type): bool
|
||||
{
|
||||
foreach ($this->getFilters() as $filter) {
|
||||
if ($filter['type'] === $type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isGlobal
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setIsGlobal($isGlobal)
|
||||
{
|
||||
$this->isChanged('isGlobal', (bool) $isGlobal);
|
||||
$this->isGlobal = (bool) $isGlobal;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsGlobal()
|
||||
{
|
||||
return $this->isGlobal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy function to getIsGlobal().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isGlobal()
|
||||
{
|
||||
return $this->getIsGlobal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $alias
|
||||
*
|
||||
* @return LeadList
|
||||
*/
|
||||
public function setAlias($alias)
|
||||
{
|
||||
$this->isChanged('alias', $alias);
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAlias()
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getLeads()
|
||||
{
|
||||
return $this->leads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone entity with empty contact list.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
parent::__clone();
|
||||
|
||||
$this->id = null;
|
||||
$this->leads = new ArrayCollection();
|
||||
$this->setIsPublished(false);
|
||||
$this->setAlias('');
|
||||
$this->lastBuiltDate = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsPreferenceCenter()
|
||||
{
|
||||
return $this->isPreferenceCenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isPreferenceCenter
|
||||
*/
|
||||
public function setIsPreferenceCenter($isPreferenceCenter): void
|
||||
{
|
||||
$this->isChanged('isPreferenceCenter', (bool) $isPreferenceCenter);
|
||||
$this->isPreferenceCenter = (bool) $isPreferenceCenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated remove after several of years.
|
||||
*
|
||||
* This is needed go keep BC after we moved 'filter' and 'display' params
|
||||
* to the 'properties' array.
|
||||
*/
|
||||
private function addLegacyParams(array $filters): array
|
||||
{
|
||||
return array_map(
|
||||
function (array $filter): array {
|
||||
if (isset($filter['properties']) && $filter['properties'] && array_key_exists('filter', $filter['properties'])) {
|
||||
$filter['filter'] = $filter['properties']['filter'];
|
||||
} else {
|
||||
$filter['filter'] = $filter['filter'] ?? null;
|
||||
}
|
||||
|
||||
if (isset($filter['properties']) && $filter['properties'] && array_key_exists('display', $filter['properties'])) {
|
||||
$filter['display'] = $filter['properties']['display'];
|
||||
} else {
|
||||
$filter['display'] = $filter['display'] ?? null;
|
||||
}
|
||||
|
||||
return $filter;
|
||||
},
|
||||
$filters
|
||||
);
|
||||
}
|
||||
|
||||
public function getLastBuiltDate(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->lastBuiltDate;
|
||||
}
|
||||
|
||||
public function setLastBuiltDate(?\DateTime $lastBuiltDate): void
|
||||
{
|
||||
$this->lastBuiltDate = $lastBuiltDate;
|
||||
}
|
||||
|
||||
public function setLastBuiltDateToCurrentDatetime(): void
|
||||
{
|
||||
$now = (new DateTimeHelper())->getUtcDateTime();
|
||||
$this->setLastBuiltDate($now);
|
||||
}
|
||||
|
||||
public function getLastBuiltTime(): ?float
|
||||
{
|
||||
return $this->lastBuiltTime;
|
||||
}
|
||||
|
||||
public function setLastBuiltTime(?float $lastBuiltTime): void
|
||||
{
|
||||
$this->lastBuiltTime = $lastBuiltTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function setFirstFilterGlueToAnd(array $filters): array
|
||||
{
|
||||
foreach ($filters as &$filter) {
|
||||
$filter['glue'] = 'and';
|
||||
break;
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,867 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadList>
|
||||
*/
|
||||
class LeadListRepository extends CommonRepository
|
||||
{
|
||||
use OperatorListTrait; // @deprecated to be removed in Mautic 3. Not used inside this class.
|
||||
|
||||
use ExpressionHelperTrait;
|
||||
use RegexTrait;
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $listFiltersInnerJoinCompany = false;
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
/**
|
||||
* Flag to check if some segment filter on a company field exists.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasCompanyFilter = false;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\DBAL\Schema\Column[]
|
||||
*/
|
||||
protected $leadTableSchema;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\DBAL\Schema\Column[]
|
||||
*/
|
||||
protected $companyTableSchema;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*/
|
||||
public function getEntity($id = 0): ?LeadList
|
||||
{
|
||||
try {
|
||||
return $this
|
||||
->createQueryBuilder('l')
|
||||
->where('l.id = :listId')
|
||||
->setParameter('listId', $id)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
} catch (\Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of lists.
|
||||
*
|
||||
* @param string $alias
|
||||
* @param string $id
|
||||
* @param bool $justPublished if false, returns all published and unpublished segments
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLists(?User $user = null, $alias = '', $id = '', bool $justPublished = true)
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
$q->select('partial l.{id, name, alias}');
|
||||
|
||||
if ($justPublished) {
|
||||
$q->andWhere($q->expr()->eq('l.isPublished', ':true'))
|
||||
->setParameter('true', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$q->andWhere($q->expr()->eq('l.isGlobal', ':true'));
|
||||
$q->orWhere('l.createdBy = :user');
|
||||
$q->setParameter('user', $user->getId());
|
||||
}
|
||||
|
||||
if (!empty($alias)) {
|
||||
$q->andWhere('l.alias = :alias');
|
||||
$q->setParameter('alias', $alias);
|
||||
}
|
||||
|
||||
if (!empty($id)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->neq('l.id', $id)
|
||||
);
|
||||
}
|
||||
|
||||
$q->orderBy('l.name');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lists for a specific lead.
|
||||
*
|
||||
* @param int|Lead[] $lead Lead ID or array of Leads
|
||||
* @param bool $forList
|
||||
* @param bool $singleArrayHydration
|
||||
* @param bool $isPublic
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLeadLists($lead, $forList = false, $singleArrayHydration = false, $isPublic = false, $isPreferenceCenter = false)
|
||||
{
|
||||
if (is_array($lead)) {
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
if ($forList) {
|
||||
$q->select('partial l.{id, alias, name}, partial il.{lead, list, dateAdded, manuallyAdded, manuallyRemoved}');
|
||||
} else {
|
||||
$q->select('l, partial lead.{id}');
|
||||
}
|
||||
|
||||
$q->leftJoin('l.leads', 'il')
|
||||
->leftJoin('il.lead', 'lead');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->in('lead.id', ':leads'),
|
||||
$q->expr()->in('il.manuallyRemoved', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('leads', $lead)
|
||||
->setParameter('false', false, 'boolean');
|
||||
|
||||
if ($isPublic) {
|
||||
$q->andWhere($q->expr()->eq('l.isGlobal', ':isPublic'))
|
||||
->setParameter('isPublic', true, 'boolean');
|
||||
}
|
||||
if ($isPreferenceCenter) {
|
||||
$q->andWhere($q->expr()->eq('l.isPreferenceCenter', ':isPreferenceCenter'))
|
||||
->setParameter('isPreferenceCenter', true, 'boolean');
|
||||
}
|
||||
$result = $q->getQuery()->getArrayResult();
|
||||
$return = [];
|
||||
foreach ($result as $r) {
|
||||
foreach ($r['leads'] as $l) {
|
||||
$return[$l['lead_id']][$r['id']] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
} else {
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
if ($forList) {
|
||||
$q->select('partial l.{id, alias, name}, partial il.{lead, list, dateAdded, manuallyAdded, manuallyRemoved}');
|
||||
} else {
|
||||
$q->select('l');
|
||||
}
|
||||
|
||||
$q->leftJoin('l.leads', 'il');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('IDENTITY(il.lead)', (int) $lead),
|
||||
$q->expr()->in('il.manuallyRemoved', ':false')
|
||||
)
|
||||
)
|
||||
->setParameter('false', false, 'boolean');
|
||||
|
||||
if ($isPublic) {
|
||||
$q->andWhere($q->expr()->eq('l.isGlobal', ':isPublic'))
|
||||
->setParameter('isPublic', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($isPreferenceCenter) {
|
||||
$q->andWhere($q->expr()->eq('l.isPreferenceCenter', ':isPreferenceCenter'))
|
||||
->setParameter('isPreferenceCenter', true, 'boolean');
|
||||
}
|
||||
|
||||
return ($singleArrayHydration) ? $q->getQuery()->getArrayResult() : $q->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Lead segments by ids.
|
||||
*/
|
||||
public function checkLeadSegmentsByIds(Lead $lead, $ids): bool
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->select('ll.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
|
||||
->where(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->in('ll.leadlist_id', ':ids'),
|
||||
$qb->expr()->eq('ll.lead_id', ':leadId'),
|
||||
$qb->expr()->eq('ll.manually_removed', 0)
|
||||
)
|
||||
)
|
||||
->setParameter('leadId', $lead->getId())
|
||||
->setParameter('ids', $ids, ArrayParameterType::INTEGER);
|
||||
|
||||
return (bool) $qb->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of global lists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGlobalLists()
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
$q->select('partial l.{id, name, alias}')
|
||||
->where($q->expr()->eq('l.isPublished', 'true'))
|
||||
->setParameter('true', true, 'boolean')
|
||||
->andWhere($q->expr()->eq('l.isGlobal', ':true'))
|
||||
->orderBy('l.name');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of global lists.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPreferenceCenterList()
|
||||
{
|
||||
$q = $this->getEntityManager()->createQueryBuilder()
|
||||
->from(LeadList::class, 'l', 'l.id');
|
||||
|
||||
$q->select('partial l.{id, name, publicName, alias}')
|
||||
->where($q->expr()->eq('l.isPublished', 'true'))
|
||||
->setParameter('true', true, 'boolean')
|
||||
->andWhere($q->expr()->eq('l.isPreferenceCenter', ':true'))
|
||||
->orderBy('l.name');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of leads that belong to the list.
|
||||
*
|
||||
* @param int|int[] $listIds
|
||||
*
|
||||
* @return array|int
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getLeadCount($listIds)
|
||||
{
|
||||
if (!is_array($listIds)) {
|
||||
$listIds = [$listIds];
|
||||
}
|
||||
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('count(l.lead_id) as thecount, l.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'l');
|
||||
|
||||
$countListIds = count($listIds);
|
||||
|
||||
if (1 === $countListIds) {
|
||||
$q = $this->forceUseIndex($q, MAUTIC_TABLE_PREFIX.'manually_removed');
|
||||
$expression = $q->expr()->eq('l.leadlist_id', $listIds[0]);
|
||||
} else {
|
||||
$expression = $q->expr()->in('l.leadlist_id', $listIds);
|
||||
}
|
||||
|
||||
$q->where(
|
||||
$expression,
|
||||
$q->expr()->eq('l.manually_removed', ':false')
|
||||
)
|
||||
->setParameter('false', false, 'boolean')
|
||||
->groupBy('l.leadlist_id');
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
foreach ($result as $r) {
|
||||
$return[$r['leadlist_id']] = (int) $r['thecount'];
|
||||
}
|
||||
|
||||
// Ensure lists without leads have a value
|
||||
foreach ($listIds as $l) {
|
||||
if (!isset($return[$l])) {
|
||||
$return[$l] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (1 === $countListIds) ? $return[$listIds[0]] : $return;
|
||||
}
|
||||
|
||||
private function forceUseIndex(QueryBuilder $qb, string $indexName): QueryBuilder
|
||||
{
|
||||
$fromPart = $qb->getQueryPart('from');
|
||||
$fromPart[0]['alias'] = sprintf('%s USE INDEX (%s)', $fromPart[0]['alias'], $indexName);
|
||||
$qb->resetQueryPart('from');
|
||||
$qb->from($fromPart[0]['table'], $fromPart[0]['alias']);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function arrangeFilters($filters): array
|
||||
{
|
||||
$objectFilters = [];
|
||||
if (empty($filters)) {
|
||||
$objectFilters['lead'][] = $filters;
|
||||
}
|
||||
foreach ($filters as $filter) {
|
||||
$object = $filter['object'] ?? 'lead';
|
||||
switch ($object) {
|
||||
case 'company':
|
||||
$objectFilters['company'][] = $filter;
|
||||
break;
|
||||
default:
|
||||
$objectFilters['lead'][] = $filter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $objectFilters;
|
||||
}
|
||||
|
||||
public function setDispatcher(EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, $leadId = null, array $subQueryFilters = [])
|
||||
{
|
||||
$subQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subExpr = [];
|
||||
|
||||
foreach ($subQueryFilters as $subColumn => $subParameter) {
|
||||
$subExpr[] = $subQb->expr()->eq($subColumn, ":$subParameter");
|
||||
}
|
||||
|
||||
if ('leads' !== $table) {
|
||||
$subExpr[] = $subQb->expr()->eq($alias.'.lead_id', 'l.id');
|
||||
}
|
||||
|
||||
// Specific lead
|
||||
if (!empty($leadId)) {
|
||||
$columnName = ('leads' === $table) ? 'id' : 'lead_id';
|
||||
$subExpr[] = $subQb->expr()->eq($alias.'.'.$columnName, $leadId);
|
||||
}
|
||||
|
||||
if (null !== $value && !empty($column)) {
|
||||
$subFilterParamter = $this->generateRandomParameterName();
|
||||
$subFunc = 'eq';
|
||||
if (is_array($value)) {
|
||||
$subFunc = 'in';
|
||||
$subExpr[] = $subQb->expr()->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter");
|
||||
$parameters[$subFilterParamter] = ['value' => $value, 'type' => ArrayParameterType::STRING];
|
||||
} else {
|
||||
$parameters[$subFilterParamter] = $value;
|
||||
}
|
||||
|
||||
$subExpr = $subQb->expr()->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter");
|
||||
}
|
||||
|
||||
$subQb->expr()->and(...$subExpr);
|
||||
|
||||
$subQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.$table, $alias)
|
||||
->where($subExpr);
|
||||
|
||||
return $subQb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'l.name',
|
||||
'l.alias',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = parent::addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.lead.list.searchcommand.isglobal'):
|
||||
case $this->translator->trans('mautic.lead.list.searchcommand.isglobal', [], null, 'en_US'):
|
||||
$expr = $q->expr()->eq('l.isGlobal', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.name', [], null, 'en_US'):
|
||||
$expr = $q->expr()->like('l.name', ':'.$unique);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'leadlist_id',
|
||||
'lead_list_projects_xref',
|
||||
'l',
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [
|
||||
$expr,
|
||||
$parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.lead.list.searchcommand.isglobal',
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.name',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.core.searchcommand.category',
|
||||
'mautic.project.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
public function getRelativeDateStrings(): array
|
||||
{
|
||||
$keys = self::getRelativeDateTranslationKeys();
|
||||
|
||||
$strings = [];
|
||||
foreach ($keys as $key) {
|
||||
$strings[$key] = $this->translator->trans($key);
|
||||
}
|
||||
|
||||
return $strings;
|
||||
}
|
||||
|
||||
public static function getRelativeDateTranslationKeys(): array
|
||||
{
|
||||
return [
|
||||
'mautic.lead.list.month_last',
|
||||
'mautic.lead.list.month_next',
|
||||
'mautic.lead.list.month_this',
|
||||
'mautic.lead.list.today',
|
||||
'mautic.lead.list.tomorrow',
|
||||
'mautic.lead.list.yesterday',
|
||||
'mautic.lead.list.week_last',
|
||||
'mautic.lead.list.week_next',
|
||||
'mautic.lead.list.week_this',
|
||||
'mautic.lead.list.year_last',
|
||||
'mautic.lead.list.year_next',
|
||||
'mautic.lead.list.year_this',
|
||||
'mautic.lead.list.anniversary',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['l.name', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'l';
|
||||
}
|
||||
|
||||
public function leadListExists(int $id): bool
|
||||
{
|
||||
$tableName = MAUTIC_TABLE_PREFIX.'lead_lists';
|
||||
$result = (int) $this->getEntityManager()->getConnection()
|
||||
->executeQuery("SELECT EXISTS(SELECT 1 FROM {$tableName} WHERE id = {$id})")
|
||||
->fetchOne();
|
||||
|
||||
return 1 === $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of campaigns related to this segment.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getSegmentCampaigns(int $segmentId): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('clx.campaign_id, c.name')
|
||||
->distinct()
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref', 'clx')
|
||||
->join('clx', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = clx.campaign_id');
|
||||
$q->where(
|
||||
$q->expr()->eq('clx.leadlist_id', $segmentId)
|
||||
);
|
||||
|
||||
$lists = [];
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $row) {
|
||||
$lists[$row['campaign_id']] = $row['name'];
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
public function isContactInAnySegment(int $contactId): bool
|
||||
{
|
||||
$tableName = MAUTIC_TABLE_PREFIX.'lead_lists_leads';
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT leadlist_id
|
||||
FROM $tableName
|
||||
WHERE lead_id = ?
|
||||
AND manually_removed = 0
|
||||
LIMIT 1
|
||||
SQL;
|
||||
|
||||
$segmentIds = $this->getEntityManager()->getConnection()
|
||||
->executeQuery(
|
||||
$sql,
|
||||
[$contactId],
|
||||
[\PDO::PARAM_INT]
|
||||
)
|
||||
->fetchFirstColumn();
|
||||
|
||||
return !empty($segmentIds);
|
||||
}
|
||||
|
||||
public function isNotContactInAnySegment(int $contactId): bool
|
||||
{
|
||||
return !$this->isContactInAnySegment($contactId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isContactInSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
return !empty($segmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isNotContactInSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
if (empty($segmentIds)) {
|
||||
return true; // Contact is not associated wit any segment
|
||||
}
|
||||
|
||||
foreach ($expectedSegmentIds as $expectedSegmentId) {
|
||||
if (in_array($expectedSegmentId, $segmentIds)) { // No exact type comparison used!
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isContactInAllSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
return count($segmentIds) === count($expectedSegmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*/
|
||||
public function isNotContactInAllSegments(int $contactId, array $expectedSegmentIds): bool
|
||||
{
|
||||
$segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds);
|
||||
|
||||
return [] === $segmentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedSegmentIds
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function fetchContactToSegmentIdsRelationships(int $contactId, array $expectedSegmentIds): array
|
||||
{
|
||||
$tableName = MAUTIC_TABLE_PREFIX.'lead_lists_leads';
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT leadlist_id
|
||||
FROM $tableName
|
||||
WHERE lead_id = ?
|
||||
AND leadlist_id IN (?)
|
||||
AND manually_removed = 0
|
||||
SQL;
|
||||
|
||||
return $this->getEntityManager()->getConnection()
|
||||
->executeQuery(
|
||||
$sql,
|
||||
[$contactId, $expectedSegmentIds],
|
||||
[
|
||||
\PDO::PARAM_INT,
|
||||
ArrayParameterType::INTEGER,
|
||||
]
|
||||
)
|
||||
->fetchFirstColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getAllSegments(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('title', 'title');
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
$rsm->addScalarResult('is_published', 'is_published');
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
ll.name as title,
|
||||
ll.id as item_id,
|
||||
ll.is_published as is_published
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'lead_lists ll', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getCampaignEntryPoints(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'campaign_leadlist_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getEmailIncludeExcludeList(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'email_list_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
$included = $query->getResult();
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'email_list_excluded
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
$excluded = $query->getResult();
|
||||
|
||||
return array_merge($included, $excluded);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getCampaignChangeSegmentAction(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('properties', 'properties');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
properties
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'campaign_events ce
|
||||
WHERE ce.type = \'lead.changelist\'', $rsm);
|
||||
|
||||
$segmentIds = [];
|
||||
foreach ($query->getResult() as $property) {
|
||||
$property = unserialize($property['properties']);
|
||||
$segmentIds = array_merge($property['addToLists'], $property['removeFromLists'], $segmentIds);
|
||||
}
|
||||
|
||||
return array_map(fn ($segment) => ['item_id' => (string) $segment], $segmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFilterSegmentsAction(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('filters', 'filters');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
filters
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'lead_lists', $rsm);
|
||||
|
||||
$childSegmentIds = [];
|
||||
|
||||
foreach ($query->getResult() as $rowFilters) {
|
||||
$segmentMembershipFilters = array_filter(
|
||||
unserialize($rowFilters['filters']),
|
||||
fn (array $filter) => 'leadlist' === $filter['type']
|
||||
);
|
||||
|
||||
foreach ($segmentMembershipFilters as $filter) {
|
||||
if (is_array($filter['properties']['filter'])) {
|
||||
foreach ($filter['properties']['filter'] as $childSegmentId) {
|
||||
$childSegmentIds[] = ['item_id' => (string) $childSegmentId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $childSegmentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getLeadListLeads(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'lead_lists_leads
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getNotificationIncludedList(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'push_notification_list_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getSMSIncludedList(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('item_id', 'item_id');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
leadlist_id as item_id
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'sms_message_list_xref
|
||||
GROUP BY leadlist_id', $rsm);
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFormAction(): array
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('properties', 'properties');
|
||||
|
||||
$query = $this->getEntityManager()->createNativeQuery('SELECT
|
||||
properties
|
||||
FROM '.MAUTIC_TABLE_PREFIX.'form_actions fa
|
||||
WHERE fa.type = \'lead.changelist\'', $rsm);
|
||||
|
||||
$segmentIds = [];
|
||||
foreach ($query->getResult() as $property) {
|
||||
$property = unserialize($property['properties']);
|
||||
$segmentIds = array_merge($property['addToLists'], $property['removeFromLists'], $segmentIds);
|
||||
}
|
||||
|
||||
return array_map(fn ($segment) => ['item_id' => (string) $segment], $segmentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getLeadSegmentIds(int $leadId): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('ll.id')
|
||||
->from(LeadList::class, 'll')
|
||||
->innerJoin('ll.leads', 'l')
|
||||
->where(
|
||||
$qb->expr()->eq('l.lead', ':leadId')
|
||||
)
|
||||
->setParameter('leadId', $leadId);
|
||||
$result = $qb->getQuery()->getArrayResult();
|
||||
|
||||
return array_column($result, 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
class LeadNote extends FormEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $type = 'general';
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateTime;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_notes')
|
||||
->setCustomRepositoryClass(LeadNoteRepository::class);
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', false, 'notes');
|
||||
|
||||
$builder->addField('text', 'text');
|
||||
|
||||
$builder->createField('type', 'string')
|
||||
->length(50)
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('dateTime', 'datetime')
|
||||
->columnName('date_time')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('leadNote')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'text',
|
||||
'type',
|
||||
'dateTime',
|
||||
'lead',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set text.
|
||||
*
|
||||
* @param string $text
|
||||
*
|
||||
* @return LeadNote
|
||||
*/
|
||||
public function setText($text)
|
||||
{
|
||||
$this->isChanged('text', $text);
|
||||
$this->text = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getText()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return LeadNote
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->isChanged('type', $type);
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation rules.
|
||||
*/
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('text', new NotBlank(
|
||||
['message' => 'mautic.lead.note.text.notblank']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
public function setLead(Lead $lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
public function convertToArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDateTime()
|
||||
{
|
||||
return $this->dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $dateTime
|
||||
*/
|
||||
public function setDateTime($dateTime): void
|
||||
{
|
||||
$this->dateTime = $dateTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadNote>
|
||||
*/
|
||||
class LeadNoteRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* {@inhertidoc}.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder('n')
|
||||
->select('n');
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getNoteCount($leadId, $filter = null, $noteTypes = null)
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder('n');
|
||||
$q->select('count(n.id) as note_count')
|
||||
->where($q->expr()->eq('IDENTITY(n.lead)', ':lead'))
|
||||
->setParameter('lead', $leadId);
|
||||
|
||||
if (null != $filter) {
|
||||
$q->andWhere(
|
||||
$q->expr()->like('n.text', ':filter')
|
||||
)->setParameter('filter', '%'.$filter.'%');
|
||||
}
|
||||
|
||||
if (null != $noteTypes) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('n.type', ':noteTypes')
|
||||
)->setParameter('noteTypes', $noteTypes);
|
||||
}
|
||||
|
||||
$results = $q->getQuery()->getArrayResult();
|
||||
|
||||
return $results[0]['note_count'];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'n';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'n.text',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
$command = $filter->command;
|
||||
$string = $filter->string;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
[$expr, $parameters] = parent::addSearchCommandWhereClause($q, $filter);
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.type'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.type', [], null, 'en_US'):
|
||||
switch ($string) {
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.general'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.general', [], null, 'en_US'):
|
||||
$filter->string = 'general';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.call'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.call', [], null, 'en_US'):
|
||||
$filter->string = 'call';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.email'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.email', [], null, 'en_US'):
|
||||
$filter->string = 'email';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.meeting'):
|
||||
case $this->translator->trans('mautic.lead.note.searchcommand.meeting', [], null, 'en_US'):
|
||||
$filter->string = 'meeting';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
}
|
||||
$expr = $q->expr()->eq('n.type', ":$unique");
|
||||
$filter->strict = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [
|
||||
$expr,
|
||||
$parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string[]>
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.lead.note.searchcommand.type' => [
|
||||
'mautic.lead.note.searchcommand.general',
|
||||
'mautic.lead.note.searchcommand.call',
|
||||
'mautic.lead.note.searchcommand.email',
|
||||
'mautic.lead.note.searchcommand.meeting',
|
||||
],
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$this->_em->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'lead_notes')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class ListLead
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const TABLE_NAME = 'lead_lists_leads';
|
||||
|
||||
/**
|
||||
* @var LeadList
|
||||
**/
|
||||
private $list;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $manuallyRemoved = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $manuallyAdded = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('lead_lists_leads')
|
||||
->setCustomRepositoryClass(ListLeadRepository::class);
|
||||
|
||||
$builder->createManyToOne('list', 'LeadList')
|
||||
->isPrimaryKey()
|
||||
->inversedBy('leads')
|
||||
->addJoinColumn('leadlist_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', true);
|
||||
|
||||
$builder->addDateAdded();
|
||||
|
||||
$builder->createField('manuallyRemoved', 'boolean')
|
||||
->columnName('manually_removed')
|
||||
->build();
|
||||
|
||||
$builder->createField('manuallyAdded', 'boolean')
|
||||
->columnName('manually_added')
|
||||
->build();
|
||||
|
||||
$builder->addIndex(['manually_removed'], 'manually_removed');
|
||||
$builder->addIndex(['lead_id', 'leadlist_id', 'manually_removed'], 'lead_id_lists_id_removed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
*/
|
||||
public function setDateAdded($date): void
|
||||
{
|
||||
$this->dateAdded = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadList
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadList $leadList
|
||||
*/
|
||||
public function setList($leadList): void
|
||||
{
|
||||
$this->list = $leadList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyRemoved
|
||||
*/
|
||||
public function setManuallyRemoved($manuallyRemoved): void
|
||||
{
|
||||
$this->manuallyRemoved = $manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyRemoved()
|
||||
{
|
||||
return $this->manuallyRemoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $manuallyAdded
|
||||
*/
|
||||
public function setManuallyAdded($manuallyAdded): void
|
||||
{
|
||||
$this->manuallyAdded = $manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function wasManuallyAdded()
|
||||
{
|
||||
return $this->manuallyAdded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<ListLead>
|
||||
*/
|
||||
class ListLeadRepository extends CommonRepository
|
||||
{
|
||||
public const DELETE_BATCH_SIZE = 5000;
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
// First check to ensure the $toLead doesn't already exist
|
||||
$results = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('l.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'l')
|
||||
->where('l.lead_id = '.$toLeadId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$lists = [];
|
||||
foreach ($results as $r) {
|
||||
$lists[] = $r['leadlist_id'];
|
||||
}
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'lead_lists_leads')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId);
|
||||
|
||||
if (!empty($lists)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->notIn('leadlist_id', $lists)
|
||||
)->executeStatement();
|
||||
|
||||
// Delete remaining leads as the new lead already belongs
|
||||
$this->_em->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'lead_lists_leads')
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
} else {
|
||||
$q->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*/
|
||||
public function getContactsCountBySegment(int $segmentId, array $filters = []): int
|
||||
{
|
||||
$qb = $this->createQueryBuilder('ll');
|
||||
$qb->select('count(ll.list) as count')
|
||||
->where('ll.list = :segmentId')
|
||||
->setParameter('segmentId', $segmentId);
|
||||
|
||||
foreach ($filters as $colName => $val) {
|
||||
$entityFieldName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $colName))));
|
||||
$qb->andWhere(sprintf('ll.%s=:%s', $entityFieldName, $entityFieldName));
|
||||
$qb->setParameter($entityFieldName, $val);
|
||||
}
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function deleteAnonymousContacts(): int
|
||||
{
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
$tableName = $this->getTableName();
|
||||
$leadsTableName = MAUTIC_TABLE_PREFIX.'leads';
|
||||
$tempTableName = 'to_delete';
|
||||
$conn->executeQuery(sprintf('DROP TEMPORARY TABLE IF EXISTS %s', $tempTableName));
|
||||
$conn->executeQuery(sprintf('CREATE TEMPORARY TABLE %s select lll.leadlist_id, lll.lead_id from %s lll join %s l on l.id = lll.lead_id where l.date_identified is null;', $tempTableName, $tableName, $leadsTableName));
|
||||
$deleteQuery = sprintf('DELETE lll FROM %s lll JOIN (SELECT leadlist_id, lead_id FROM %s LIMIT %d) d USING (leadlist_id, lead_id); ', $tableName, $tempTableName, self::DELETE_BATCH_SIZE);
|
||||
$deletedRecordCount= 0;
|
||||
while ($deletedRows = $conn->executeQuery($deleteQuery)->rowCount()) {
|
||||
$deletedRecordCount += $deletedRows;
|
||||
}
|
||||
|
||||
return $deletedRecordCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class MergeRecord
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $contact;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $mergedId;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('contact_merge_records')
|
||||
->setCustomRepositoryClass(MergeRecordRepository::class)
|
||||
->addIndex(['date_added'], 'contact_merge_date_added')
|
||||
->addIndex(['merged_id'], 'contact_merge_ids');
|
||||
|
||||
$builder->createField('id', 'integer')
|
||||
->makePrimaryKey()
|
||||
->generatedValue()
|
||||
->build();
|
||||
|
||||
$builder->addContact()
|
||||
->addDateAdded()
|
||||
->addNamedField('mergedId', 'integer', 'merged_id')
|
||||
->addField('name', 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getContact()
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setContact(Lead $contact)
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setDateAdded(?\DateTime $dateAdded = null)
|
||||
{
|
||||
if (null === $dateAdded) {
|
||||
$dateAdded = new \DateTime();
|
||||
}
|
||||
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMergedId()
|
||||
{
|
||||
return $this->mergedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $mergedId
|
||||
*
|
||||
* @return MergeRecord
|
||||
*/
|
||||
public function setMergedId($mergedId)
|
||||
{
|
||||
$this->mergedId = (int) $mergedId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<MergeRecord>
|
||||
*/
|
||||
class MergeRecordRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @return Lead|null
|
||||
*/
|
||||
public function findMergedContact($id)
|
||||
{
|
||||
/** @var MergeRecord $record */
|
||||
if ($record = $this->findOneBy(['mergedId' => (int) $id], ['dateAdded' => 'desc'])) {
|
||||
$contact = $record->getContact();
|
||||
|
||||
// Clear these records from the EM so that subsequent fetches don't return deleted entities
|
||||
$this->getEntityManager()->detach($record);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of subseqent merges by cascading records to the latest lead that was merged into.
|
||||
*/
|
||||
public function moveMergeRecord($fromId, $toId): void
|
||||
{
|
||||
$this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'contact_merge_records')
|
||||
->set('contact_id', (int) $toId)
|
||||
->where('contact_id = '.(int) $fromId)
|
||||
->executeQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Segment\OperatorOptions;
|
||||
|
||||
trait OperatorListTrait
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<string, array<int, string>>>
|
||||
*/
|
||||
protected $typeOperators = [
|
||||
'text' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
'select' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::INCLUDING_ANY,
|
||||
OperatorOptions::EXCLUDING_ANY,
|
||||
OperatorOptions::INCLUDING_ALL,
|
||||
OperatorOptions::EXCLUDING_ALL,
|
||||
],
|
||||
],
|
||||
'bool' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
],
|
||||
],
|
||||
'default' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::GREATER_THAN,
|
||||
OperatorOptions::GREATER_THAN_OR_EQUAL,
|
||||
OperatorOptions::LESS_THAN,
|
||||
OperatorOptions::LESS_THAN_OR_EQUAL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::BETWEEN,
|
||||
OperatorOptions::NOT_BETWEEN,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
'multiselect' => [
|
||||
'include' => [
|
||||
OperatorOptions::INCLUDING_ANY,
|
||||
OperatorOptions::EXCLUDING_ANY,
|
||||
OperatorOptions::INCLUDING_ALL,
|
||||
OperatorOptions::EXCLUDING_ALL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
],
|
||||
],
|
||||
'date' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::GREATER_THAN,
|
||||
OperatorOptions::GREATER_THAN_OR_EQUAL,
|
||||
OperatorOptions::LESS_THAN,
|
||||
OperatorOptions::LESS_THAN_OR_EQUAL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::BETWEEN,
|
||||
OperatorOptions::NOT_BETWEEN,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::DATE,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
'lookup_id' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
],
|
||||
],
|
||||
'number' => [
|
||||
'include' => [
|
||||
OperatorOptions::EQUAL_TO,
|
||||
OperatorOptions::NOT_EQUAL_TO,
|
||||
OperatorOptions::GREATER_THAN,
|
||||
OperatorOptions::GREATER_THAN_OR_EQUAL,
|
||||
OperatorOptions::LESS_THAN,
|
||||
OperatorOptions::LESS_THAN_OR_EQUAL,
|
||||
OperatorOptions::EMPTY,
|
||||
OperatorOptions::NOT_EMPTY,
|
||||
OperatorOptions::LIKE,
|
||||
OperatorOptions::NOT_LIKE,
|
||||
OperatorOptions::REGEXP,
|
||||
OperatorOptions::NOT_REGEXP,
|
||||
OperatorOptions::STARTS_WITH,
|
||||
OperatorOptions::ENDS_WITH,
|
||||
OperatorOptions::CONTAINS,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @deprecated to be removed in Mautic 3. Use FilterOperatorProvider::getAllOperators() instead.
|
||||
*
|
||||
* @param string|null $operator
|
||||
*
|
||||
* @return array<string,array<string,string>>|array<string,string>
|
||||
*/
|
||||
public function getFilterExpressionFunctions($operator = null)
|
||||
{
|
||||
$operatorOption = OperatorOptions::getFilterExpressionFunctions();
|
||||
|
||||
return (null === $operator) ? $operatorOption : $operatorOption[$operator];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|mixed[]|null $type
|
||||
* @param mixed[] $overrideHiddenTypes
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getOperatorsForFieldType($type = null, $overrideHiddenTypes = [])
|
||||
{
|
||||
static $processedTypes = [];
|
||||
|
||||
if (is_array($type)) {
|
||||
return $this->getOperatorChoiceList($type, $overrideHiddenTypes);
|
||||
} elseif (array_key_exists($type, $processedTypes)) {
|
||||
return $processedTypes[$type];
|
||||
}
|
||||
|
||||
$type = $this->normalizeType($type);
|
||||
|
||||
if (null === $type) {
|
||||
foreach ($this->typeOperators as $type => $def) {
|
||||
if (!array_key_exists($type, $processedTypes)) {
|
||||
$processedTypes[$type] = $this->getOperatorChoiceList($def, $overrideHiddenTypes);
|
||||
}
|
||||
}
|
||||
|
||||
return $processedTypes;
|
||||
}
|
||||
|
||||
$processedTypes[$type] = $this->getOperatorChoiceList($this->typeOperators[$type], $overrideHiddenTypes);
|
||||
|
||||
return $processedTypes[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $definition
|
||||
* @param mixed[] $overrideHiddenOperators
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getOperatorChoiceList($definition, $overrideHiddenOperators = []): array
|
||||
{
|
||||
static $operatorChoices = [];
|
||||
if (empty($operatorChoices)) {
|
||||
$operatorList = $this->getFilterExpressionFunctions();
|
||||
$operatorChoices = [];
|
||||
foreach ($operatorList as $operator => $def) {
|
||||
if (empty($def['hide']) || in_array($operator, $overrideHiddenOperators)) {
|
||||
$operatorChoices[$operator] = $def['label'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$choices = $operatorChoices;
|
||||
if (isset($definition['include'])) {
|
||||
// Inclusive operators
|
||||
$choices = array_intersect_key($choices, array_flip($definition['include']));
|
||||
} elseif (isset($definition['exclude'])) {
|
||||
// Exclusive operators
|
||||
$choices = array_diff_key($choices, array_flip($definition['exclude']));
|
||||
}
|
||||
|
||||
if (property_exists($this, 'translator')) { // @phpstan-ignore-line based on https://github.com/phpstan/phpstan/issues/9095 (Call to function property_exists() with ... 'translator' will always evaluate to false.)
|
||||
foreach ($choices as $value => $label) {
|
||||
$choices[$value] = $this->translator->trans($label);
|
||||
}
|
||||
}
|
||||
|
||||
return array_flip($choices);
|
||||
}
|
||||
|
||||
protected function normalizeType(mixed $type): mixed
|
||||
{
|
||||
if (null === $type) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if ('boolean' === $type) {
|
||||
return 'bool';
|
||||
}
|
||||
|
||||
if (in_array($type, ['country', 'timezone', 'region', 'locale'])) {
|
||||
return 'select';
|
||||
}
|
||||
|
||||
if (in_array($type, ['lookup', 'text', 'email', 'url', 'email', 'tel'])) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
if ('datetime' === $type) {
|
||||
return 'date';
|
||||
}
|
||||
|
||||
if (!array_key_exists($type, $this->typeOperators)) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user