Merge pull request #23374 from eileenmcnaughton/549_up
[civicrm-core.git] / CRM / Utils / Address / BatchUpdate.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035 11
50bfb460
SB
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
50bfb460
SB
16 */
17
6a488035
TO
18/**
19 * A PHP cron script to format all the addresses in the database. Currently
20 * it only does geocoding if the geocode values are not set. At a later
21 * stage we will also handle USPS address cleanup and other formatting
22 * issues
6a488035
TO
23 */
24class CRM_Utils_Address_BatchUpdate {
25
6714d8d2
SL
26 public $start = NULL;
27 public $end = NULL;
28 public $geocoding = 1;
29 public $parse = 1;
30 public $throttle = 0;
6a488035 31
6714d8d2
SL
32 public $returnMessages = [];
33 public $returnError = 0;
6a488035 34
5bc392e6 35 /**
d4620d57 36 * Class constructor.
37 *
c490a46a 38 * @param array $params
5bc392e6 39 */
6a488035
TO
40 public function __construct($params) {
41
42 foreach ($params as $name => $value) {
43 $this->$name = $value;
44 }
45
46 // fixme: more params verification
47 }
48
5bc392e6 49 /**
d4620d57 50 * Run batch update.
51 *
5bc392e6
EM
52 * @return array
53 */
6a488035
TO
54 public function run() {
55
6a488035
TO
56 // do check for geocoding.
57 $processGeocode = FALSE;
4882d275 58 if (!CRM_Utils_GeocodeProvider::getUsableClassName()) {
c8c9ce59 59 if (CRM_Utils_String::strtobool($this->geocoding) === TRUE) {
a08aa8e8 60 $this->returnMessages[] = ts('Error: You need to set a mapping provider under Administer > System Settings > Mapping and Geocoding');
6a488035
TO
61 $this->returnError = 1;
62 $this->returnResult();
63 }
64 }
65 else {
66 $processGeocode = TRUE;
67 // user might want to over-ride.
c8c9ce59 68 if (CRM_Utils_String::strtobool($this->geocoding) === FALSE) {
6a488035
TO
69 $processGeocode = FALSE;
70 }
71 }
72
73 // do check for parse street address.
74 $parseAddress = FALSE;
75 $parseAddress = CRM_Utils_Array::value('street_address_parsing',
76 CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
77 'address_options'
78 ),
79 FALSE
80 );
81 $parseStreetAddress = FALSE;
82 if (!$parseAddress) {
c8c9ce59 83 if (CRM_Utils_String::strtobool($this->parse) === TRUE) {
a08aa8e8 84 $this->returnMessages[] = ts('Error: You need to enable Street Address Parsing under Administer > Localization > Address Settings.');
6a488035
TO
85 $this->returnError = 1;
86 return $this->returnResult();
87 }
88 }
89 else {
90 $parseStreetAddress = TRUE;
91 // user might want to over-ride.
c8c9ce59 92 if (CRM_Utils_String::strtobool($this->parse) === FALSE) {
6a488035
TO
93 $parseStreetAddress = FALSE;
94 }
95 }
96
97 // don't process.
98 if (!$parseStreetAddress && !$processGeocode) {
99 $this->returnMessages[] = ts('Error: Both Geocode mapping as well as Street Address Parsing are disabled. You must configure one or both options to use this script.');
100 $this->returnError = 1;
101 return $this->returnResult();
102 }
103
104 // do check for parse street address.
4882d275 105 return $this->processContacts($processGeocode, $parseStreetAddress);
6a488035
TO
106 }
107
5bc392e6 108 /**
d4620d57 109 * Process contacts.
110 *
d4620d57 111 * @param bool $processGeocode
112 * @param bool $parseStreetAddress
5bc392e6
EM
113 *
114 * @return array
115 * @throws Exception
116 */
4882d275 117 public function processContacts($processGeocode, $parseStreetAddress) {
6a488035 118 // build where clause.
aaa95afe 119 $clause = [];
be2fb01f 120 $params = [];
6a488035 121 if ($this->start) {
18b8253b 122 $clause[] = "( c.id >= %1 )";
be2fb01f 123 $params[1] = [$this->start, 'Integer'];
6a488035
TO
124 }
125
126 if ($this->end) {
18b8253b 127 $clause[] = "( c.id <= %2 )";
be2fb01f 128 $params[2] = [$this->end, 'Integer'];
6a488035
TO
129 }
130
131 if ($processGeocode) {
132 $clause[] = '( a.geo_code_1 is null OR a.geo_code_1 = 0 )';
133 $clause[] = '( a.geo_code_2 is null OR a.geo_code_2 = 0 )';
134 $clause[] = '( a.country_id is not null )';
135 }
136
137 $whereClause = implode(' AND ', $clause);
138
139 $query = "
aaa95afe
BS
140 SELECT c.id,
141 a.id as address_id,
142 a.street_address,
143 a.city,
144 a.postal_code,
145 a.country_id,
146 s.name as state,
147 o.name as country
148 FROM civicrm_address a
149 LEFT JOIN civicrm_contact c
150 ON a.contact_id = c.id
151 LEFT JOIN civicrm_country o
152 ON a.country_id = o.id
153 LEFT JOIN civicrm_state_province s
154 ON a.state_province_id = s.id
155 WHERE {$whereClause}
6a488035
TO
156 ORDER BY a.id
157 ";
158
159 $totalGeocoded = $totalAddresses = $totalAddressParsed = 0;
160
18b8253b 161 $dao = CRM_Core_DAO::executeQuery($query, $params);
6a488035 162
be2fb01f 163 $unparseableContactAddress = [];
6a488035
TO
164 while ($dao->fetch()) {
165 $totalAddresses++;
be2fb01f 166 $params = [
6a488035
TO
167 'street_address' => $dao->street_address,
168 'postal_code' => $dao->postal_code,
169 'city' => $dao->city,
170 'state_province' => $dao->state,
171 'country' => $dao->country,
62fe5f49 172 'country_id' => $dao->country_id,
be2fb01f 173 ];
6a488035 174
be2fb01f 175 $addressParams = [];
6a488035
TO
176
177 // process geocode.
178 if ($processGeocode) {
179 // loop through the address removing more information
180 // so we can get some geocode for a partial address
181 // i.e. city -> state -> country
182
183 $maxTries = 5;
184 do {
185 if ($this->throttle) {
186 usleep(5000000);
187 }
188
4882d275 189 CRM_Core_BAO_Address::addGeocoderData($params);
79f1148d
DL
190
191 // see if we got a geocode error, in this case we'll trigger a fatal
192 // CRM-13760
193 if (
194 isset($params['geo_code_error']) &&
195 $params['geo_code_error'] == 'OVER_QUERY_LIMIT'
196 ) {
4882d275 197 throw new CRM_Core_Exception('Aborting batch geocoding. Hit the over query limit on geocoder.');
79f1148d
DL
198 }
199
6a488035
TO
200 array_shift($params);
201 $maxTries--;
79f1148d
DL
202 } while (
203 (!isset($params['geo_code_1']) || $params['geo_code_1'] == 'null') &&
6a488035
TO
204 ($maxTries > 1)
205 );
206
79f1148d 207 if (isset($params['geo_code_1']) && $params['geo_code_1'] != 'null') {
6a488035 208 $totalGeocoded++;
f78a9f72 209 $addressParams = $params;
6a488035
TO
210 }
211 }
212
213 // parse street address
214 if ($parseStreetAddress) {
215 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($dao->street_address);
216 $success = TRUE;
217 // consider address is automatically parseable,
218 // when we should found street_number and street_name
8cc574cf 219 if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) {
6a488035
TO
220 $success = FALSE;
221 }
222
223 // do check for all elements.
224 if ($success) {
225 $totalAddressParsed++;
226 }
227 elseif ($dao->street_address) {
228 //build contact edit url,
229 //so that user can manually fill the street address fields if the street address is not parsed, CRM-5886
230 $url = CRM_Utils_System::url('civicrm/contact/add', "reset=1&action=update&cid={$dao->id}");
231 $unparseableContactAddress[] = " Contact ID: " . $dao->id . " <a href =\"$url\"> " . $dao->street_address . " </a> ";
232 // reset element values.
233 $parsedFields = array_fill_keys(array_keys($parsedFields), '');
234 }
235 $addressParams = array_merge($addressParams, $parsedFields);
236 }
237
238 // finally update address object.
239 if (!empty($addressParams)) {
240 $address = new CRM_Core_DAO_Address();
241 $address->id = $dao->address_id;
242 $address->copyValues($addressParams);
243 $address->save();
6a488035
TO
244 }
245 }
246
be2fb01f 247 $this->returnMessages[] = ts("Addresses Evaluated: %1", [
389bcebf 248 1 => $totalAddresses,
6714d8d2 249 ]) . "\n";
6a488035 250 if ($processGeocode) {
be2fb01f 251 $this->returnMessages[] = ts("Addresses Geocoded: %1", [
6714d8d2
SL
252 1 => $totalGeocoded,
253 ]) . "\n";
6a488035
TO
254 }
255 if ($parseStreetAddress) {
be2fb01f 256 $this->returnMessages[] = ts("Street Addresses Parsed: %1", [
6714d8d2
SL
257 1 => $totalAddressParsed,
258 ]) . "\n";
6a488035
TO
259 if ($unparseableContactAddress) {
260 $this->returnMessages[] = "<br />\n" . ts("Following is the list of contacts whose address is not parsed:") . "<br />\n";
261 foreach ($unparseableContactAddress as $contactLink) {
262 $this->returnMessages[] = $contactLink . "<br />\n";
263 }
264 }
265 }
266
267 return $this->returnResult();
268 }
269
5bc392e6 270 /**
d4620d57 271 * Return result.
272 *
5bc392e6
EM
273 * @return array
274 */
00be9182 275 public function returnResult() {
be2fb01f 276 $result = [];
6a488035 277 $result['is_error'] = $this->returnError;
7c7ab278 278 $result['messages'] = '';
279 // Pad message size to allow for prefix added by CRM_Core_JobManager.
280 $messageSize = 255;
281 // Ensure that each message can fit in the civicrm_job_log.data column.
282 foreach ($this->returnMessages as $message) {
283 $messageSize += strlen($message);
284 if ($messageSize > CRM_Utils_Type::BLOB_SIZE) {
285 $result['messages'] .= '...';
286 break;
287 }
288 $result['messages'] .= $message;
289 }
6a488035
TO
290 return $result;
291 }
96025800 292
6a488035 293}