├── templates └── Symbiote │ └── Addressable │ ├── Address.ss │ └── AddressMap.ss ├── CHANGELOG.md ├── .upgrade.yml ├── .gitattributes ├── _config.php ├── _config └── config.yml ├── .editorconfig ├── src ├── GeocodeServiceInterface.php ├── GeocodeServiceException.php ├── Forms │ └── RegexTextField.php ├── MapboxGeocodeService.php ├── GoogleGeocodeService.php ├── Geocodable.php └── Addressable.php ├── phpunit.xml.dist ├── composer.json ├── phpcs.xml.dist └── .scrutinizer.yml /templates/Symbiote/Addressable/Address.ss: -------------------------------------------------------------------------------- 1 |
2 | $Address
3 | $Suburb
4 | $State $Postcode
5 | $CountryName 6 |
-------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | For more information on changes and releases, please visit [Releases](https://github.com/symbiote/silverstripe-addressable/releases). 4 | 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | -------------------------------------------------------------------------------- /.upgrade.yml: -------------------------------------------------------------------------------- 1 | mappings: 2 | GoogleGeocoding: Symbiote\Addressable\GeocodeService 3 | Addressable: Symbiote\Addressable\Addressable 4 | Geocodable: Symbiote\Addressable\Geocodable 5 | RegexTextField: Symbiote\Addressable\Forms\RegexTextField 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /docs export-ignore 3 | /.travis.yml export-ignore 4 | /.phpunit.xml.dist export-ignore 5 | /.phpcs.xml.dist export-ignore 6 | README.md export-ignore 7 | LICENSE.md export-ignore 8 | CONTRIBUTING.md export-ignore 9 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | $FullAddress.ATT 5 | 6 |
7 | <% else %> 8 |
9 | $FullAddress.ATT 10 |
11 | <% end_if %> 12 | $Gugus 13 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | . 7 | 8 | 9 | tests/ 10 | 11 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | sanitychecks 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/GeocodeServiceException.php: -------------------------------------------------------------------------------- 1 | responseBody = $responseBody; 28 | 29 | $xml = new SimpleXMLElement($responseBody); 30 | if (isset($xml->status)) { 31 | $this->status = (string)$xml->status; 32 | } 33 | } 34 | 35 | public function getResponse() 36 | { 37 | return $this->responseBody; 38 | } 39 | 40 | /** 41 | * Return "status" values: 42 | * - ZERO_RESULTS 43 | * - OVER_QUERY_LIMIT 44 | * 45 | * @return string 46 | */ 47 | public function getStatus() 48 | { 49 | return $this->status; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Forms/RegexTextField.php: -------------------------------------------------------------------------------- 1 | regex; 28 | } 29 | 30 | /** 31 | * @param string $regex 32 | */ 33 | public function setRegex($regex) 34 | { 35 | $this->regex = $regex; 36 | } 37 | 38 | public function validate($validator) 39 | { 40 | if ($this->value && $this->regex) { 41 | if (!preg_match($this->regex, (string) $this->value)) { 42 | $name = $this->Title() ?: $this->name; 43 | $message = _t('RegexTextField.VALIDATE', 'Please enter a valid format for "%s".'); 44 | $validator->validationError($this->name, sprintf($message, $name), 'validation'); 45 | return false; 46 | } 47 | } 48 | 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symbiote/silverstripe-addressable", 3 | "description": "SilverStripe addressable and geocoding module", 4 | "type": "silverstripe-vendormodule", 5 | "keywords": [ 6 | "silverstripe", 7 | "symbiote", 8 | "addressable", 9 | "geocoding" 10 | ], 11 | "license": "BSD-3-Clause", 12 | "authors": [ 13 | { 14 | "name": "Symbiote", 15 | "homepage": "https://www.symbiote.com.au/" 16 | }, 17 | { 18 | "name": "Marcus Nyeholt", 19 | "email": "nyeholt@symbiote.com.au" 20 | } 21 | ], 22 | "require": { 23 | "silverstripe/framework": "^4.12||^5", 24 | "guzzlehttp/guzzle": "^7" 25 | }, 26 | "require-dev": { 27 | "squizlabs/php_codesniffer": "^3.5", 28 | "phpunit/phpunit": "^9.6" 29 | }, 30 | "scripts": { 31 | "phpcbf": "phpcbf src/ src/ tests/", 32 | "phpcs": "phpcs src/ src/ tests/" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Symbiote\\Addressable\\": "src/", 37 | "Symbiote\\Addressable\\Tests\\": "tests/" 38 | } 39 | }, 40 | "extra": { 41 | "branch-alias": { 42 | "dev-master": "5.2.x-dev" 43 | } 44 | }, 45 | "replace": { 46 | "ajshort/silverstripe-addressable": "self.version", 47 | "silverstripe-australia/addressable": "self.version" 48 | }, 49 | "prefer-stable": true, 50 | "minimum-stability": "dev" 51 | } 52 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeSniffer ruleset for SilverStripe coding conventions. 4 | 5 | 6 | */vendor/* 7 | */thirdparty/* 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | */SSTemplateParser.php$ 43 | */_fakewebroot/* 44 | */fixtures/* 45 | 46 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | checks: 4 | php: 5 | verify_property_names: true 6 | verify_argument_usable_as_reference: true 7 | verify_access_scope_valid: true 8 | useless_calls: true 9 | use_statement_alias_conflict: true 10 | variable_existence: true 11 | unused_variables: true 12 | unused_properties: true 13 | unused_parameters: true 14 | unused_methods: true 15 | unreachable_code: true 16 | too_many_arguments: true 17 | sql_injection_vulnerabilities: true 18 | simplify_boolean_return: true 19 | side_effects_or_types: true 20 | security_vulnerabilities: true 21 | return_doc_comments: true 22 | return_doc_comment_if_not_inferrable: true 23 | require_scope_for_properties: true 24 | require_scope_for_methods: true 25 | require_php_tag_first: true 26 | psr2_switch_declaration: true 27 | psr2_class_declaration: true 28 | property_assignments: true 29 | prefer_while_loop_over_for_loop: true 30 | precedence_mistakes: true 31 | precedence_in_conditions: true 32 | phpunit_assertions: true 33 | php5_style_constructor: true 34 | parse_doc_comments: true 35 | parameter_non_unique: true 36 | parameter_doc_comments: true 37 | param_doc_comment_if_not_inferrable: true 38 | optional_parameters_at_the_end: true 39 | one_class_per_file: true 40 | no_unnecessary_if: true 41 | no_trailing_whitespace: true 42 | no_property_on_interface: true 43 | no_non_implemented_abstract_methods: true 44 | no_error_suppression: true 45 | no_duplicate_arguments: true 46 | no_commented_out_code: true 47 | newline_at_end_of_file: true 48 | missing_arguments: true 49 | method_calls_on_non_object: true 50 | instanceof_class_exists: true 51 | foreach_traversable: true 52 | fix_line_ending: true 53 | fix_doc_comments: true 54 | duplication: true 55 | deprecated_code_usage: true 56 | deadlock_detection_in_loops: true 57 | code_rating: true 58 | closure_use_not_conflicting: true 59 | catch_class_exists: true 60 | blank_line_after_namespace_declaration: false 61 | avoid_multiple_statements_on_same_line: true 62 | avoid_duplicate_types: true 63 | avoid_conflicting_incrementers: true 64 | avoid_closing_tag: true 65 | assignment_of_null_return: true 66 | argument_type_checks: true 67 | 68 | filter: 69 | paths: [code/*, tests/*] 70 | -------------------------------------------------------------------------------- /src/MapboxGeocodeService.php: -------------------------------------------------------------------------------- 1 | get(self::class, 'mapbox_api_url'); 40 | $key = Config::inst()->get(self::class, 'mapbox_api_key'); 41 | 42 | if (!$url) { 43 | // If no URL configured. Stop. 44 | throw new Exception('No mapbox_api_url configured. This is not allowed.'); 45 | } 46 | 47 | if (!$key) { 48 | // If no KEY configured. Stop. 49 | throw new Exception('No mapbox_api_key configured. This is not allowed.'); 50 | } 51 | 52 | // Add params 53 | $queryVars = [ 54 | 'access_token' => $key, 55 | 'type' => 'address', 56 | ]; 57 | if ($region) { 58 | $queryVars['country'] = $region; 59 | } 60 | $url .= urlencode($address) . '.json?' . http_build_query($queryVars); 61 | 62 | $client = new Client(); 63 | $response = $client->get($url); 64 | if (!$response) { 65 | throw new GeocodeServiceException('No response.', 0, ''); 66 | } 67 | $statusCode = $response->getStatusCode(); 68 | if ($statusCode !== 200) { 69 | throw new GeocodeServiceException('Unexpected status code:' . $statusCode, $statusCode, ''); 70 | } 71 | $responseBody = json_decode((string)$response->getBody()); 72 | 73 | // Error handling 74 | if ($responseBody) { 75 | if (isset($responseBody->message)) { 76 | throw new GeocodeServiceException('Error message: ' . $responseBody->message, $statusCode, $responseBody); 77 | } 78 | if (!isset($responseBody->features) || !count($responseBody->features) === 0) { 79 | throw new GeocodeServiceException('Zero results returned. Invalid status from response: ' . $status, $statusCode, $responseBody); 80 | } 81 | } else { 82 | // Fallback to full string dump 83 | $text = trim($response->getBody()); 84 | throw new GeocodeServiceException('Invalid response: ' . $text, $statusCode, $responseBody); 85 | } 86 | 87 | // We take the first match, because that's most likely the best match 88 | $feature = reset($responseBody->features); 89 | [$lng, $lat] = $feature->geometry->coordinates; 90 | 91 | return [ 92 | 'lat' => (float)$lat, 93 | 'lng' => (float)$lng 94 | ]; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/GoogleGeocodeService.php: -------------------------------------------------------------------------------- 1 | get(self::class, 'google_api_key'); 44 | if (!$key) { 45 | $key = Config::inst()->get('Symbiote\\Addressable\\GeocodeService', 'google_api_key'); 46 | } 47 | } 48 | if (!$key) { 49 | // Google Geocode API requires a key 50 | throw new \Exception('No google_api_key configured. This is not allowed.'); 51 | } 52 | return $key; 53 | } 54 | 55 | /** 56 | * Retrieve the Google Geocoding API URL from config API 57 | * @throws \Exception 58 | */ 59 | public function getApiUrl() : string 60 | { 61 | // Get the URL for the Google API (and check for legacy config) 62 | $url = Config::inst()->get(self::class, 'google_api_url'); 63 | if (!$url) { 64 | $url = Config::inst()->get('Symbiote\\Addressable\\GeocodeService', 'google_api_url'); 65 | } 66 | 67 | if (!$url) { 68 | // If no URL configured. Stop. 69 | throw new \Exception('No google_api_url configured. This is not allowed.'); 70 | } 71 | 72 | return $url; 73 | } 74 | 75 | /** 76 | * Convert an address into a latitude and longitude. 77 | * 78 | * @param string $address The address to geocode. 79 | * @param string $region An optional two letter region code. 80 | * @return array An associative array with lat and lng keys. 81 | */ 82 | public function addressToPoint($address, $region = '') 83 | { 84 | $url = $this->getApiUrl(); 85 | 86 | $key = $this->getApiKey(); 87 | 88 | // Add params 89 | $queryVars = [ 90 | 'address' => $address, 91 | 'sensor' => 'false', 92 | ]; 93 | if ($region) { 94 | $queryVars['region'] = $region; 95 | } 96 | if ($key) { 97 | $queryVars['key'] = $key; 98 | } 99 | $url .= '?' . http_build_query($queryVars); 100 | 101 | $client = new Client(); 102 | $response = $client->get($url); 103 | if (!$response) { 104 | throw new GeocodeServiceException('No response.', 0, ''); 105 | } 106 | $statusCode = $response->getStatusCode(); 107 | if ($statusCode !== 200) { 108 | throw new GeocodeServiceException('Unexpected status code:' . $statusCode, $statusCode, ''); 109 | } 110 | $responseBody = (string)$response->getBody(); 111 | $xml = new SimpleXMLElement($responseBody); 112 | if (!isset($xml->result)) { 113 | // Error handling 114 | if (isset($xml->status)) { 115 | $status = (string)$xml->status; 116 | if ($status === self::ERROR_ZERO_RESULTS) { 117 | throw new GeocodeServiceException('Zero results returned. Invalid status from response: ' . $status, $statusCode, $responseBody); 118 | } else { 119 | throw new GeocodeServiceException('Unhandled status from response: ' . $status, $statusCode, $responseBody); 120 | } 121 | } 122 | // Fallback to full string dump 123 | $text = trim($response->getBody()); 124 | throw new GeocodeServiceException('Invalid response: ' . $text, $responseBody); 125 | } 126 | $location = $xml->result->geometry->location; 127 | return [ 128 | 'lat' => (float)$location->lat, 129 | 'lng' => (float)$location->lng 130 | ]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Geocodable.php: -------------------------------------------------------------------------------- 1 | 'Boolean', 44 | 'Lat' => 'Decimal(10,7)', 45 | 'Lng' => 'Decimal(10,7)' 46 | ]; 47 | 48 | public function onBeforeWrite() 49 | { 50 | $record = $this->getOwner(); 51 | // Reset last error 52 | $record->__geocodable_exception = null; 53 | if (!Config::inst()->get(self::class, 'is_geocodable')) { 54 | // Allow user-code to disable Geocodable. This was added 55 | // so that dev/tasks that write a *lot* of Geocodable records can 56 | // ignore this expensive logic. 57 | return; 58 | } 59 | if ($record->LatLngOverride) { 60 | // A CMS user disabled automatical retrieval of Lat/Lng 61 | // and most likely input their own values. 62 | return; 63 | } 64 | if ((!$record->hasMethod('isAddressChanged') || !$record->isAddressChanged()) 65 | && $record->Lat 66 | && $record->Lng 67 | ) { 68 | // No address change, no geocoding 69 | return; 70 | } 71 | 72 | $address = $record->getFullAddress(); 73 | $region = strtolower($record->Country); 74 | 75 | $point = []; 76 | try { 77 | $point = $this->geocoder->addressToPoint($address, $region); 78 | } catch (GeocodeServiceException $e) { 79 | // Default behaviour is to ignore errors like ZERO_RESULTS or this just failing. 80 | $record->__geocodable_exception = $e; 81 | return; 82 | } 83 | if (!$point) { 84 | return; 85 | } 86 | 87 | $record->Lat = $point['lat']; 88 | $record->Lng = $point['lng']; 89 | } 90 | 91 | /** 92 | * @return GeocodeServiceException|null 93 | */ 94 | public function getLastGeocodableException() 95 | { 96 | return $this->owner->__geocodable_exception; 97 | } 98 | 99 | public function getGeocoder() 100 | { 101 | return $this->geocoder; 102 | } 103 | 104 | public function updateCMSFields(FieldList $fields) 105 | { 106 | $record = $this->getOwner(); 107 | $fields->removeByName([ 108 | 'LatLngOverride', 109 | 'Lat', 110 | 'Lng' 111 | ]); 112 | 113 | // Adds Lat/Lng fields for viewing in the CMS 114 | $compositeField = CompositeField::create(); 115 | $compositeField->push($overrideField = CheckboxField::create('LatLngOverride', 'Override Latitude and Longitude?')); 116 | $overrideField->setDescription('Check this box and save to be able to edit the latitude and longitude manually.'); 117 | if ($record->Lng && $record->Lat) { 118 | $googleMapURL = 'https://maps.google.com/?q=' . $record->Lat . ',' . $record->Lng; 119 | $googleMapDiv = '
' . $googleMapURL . '
'; 120 | $compositeField->push(LiteralField::create('MapURL_Readonly', $googleMapDiv)); 121 | } 122 | if ($record->LatLngOverride) { 123 | $compositeField->push(TextField::create('Lat', 'Lat')); 124 | $compositeField->push(TextField::create('Lng', 'Lng')); 125 | } else { 126 | $compositeField->push(ReadonlyField::create('Lat_Readonly', 'Lat', $record->Lat)); 127 | $compositeField->push(ReadonlyField::create('Lng_Readonly', 'Lng', $record->Lng)); 128 | } 129 | if ($record->hasExtension(Addressable::class)) { 130 | // If using addressable, put the fields with it 131 | $fields->addFieldToTab('Root.Address', ToggleCompositeField::create('Coordinates', 'Coordinates', $compositeField)); 132 | } elseif ($record instanceof SiteTree) { 133 | // If SIteTree but not using Addressable, put after 'Metadata' toggle composite field 134 | $fields->insertAfter('ExtraMeta', $compositeField); 135 | } else { 136 | $fields->addFieldToTab('Root.Main', ToggleCompositeField::create('Coordinates', 'Coordinates', $compositeField)); 137 | } 138 | } 139 | 140 | public function updateFrontEndFields(FieldList $fields) 141 | { 142 | $fields->removeByName([ 143 | 'LatLngOverride', 144 | 'Lat', 145 | 'Lng' 146 | ]); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Addressable.php: -------------------------------------------------------------------------------- 1 | 'Varchar(255)', 29 | 'Suburb' => 'Varchar(64)', 30 | 'State' => 'Varchar(64)', 31 | 'Postcode' => 'Varchar(10)', 32 | 'Country' => 'Varchar(2)' 33 | ]; 34 | 35 | /** 36 | * Define an array of states that the user can select from. 37 | * If no states are defined, a user can type in any plain text for their state. 38 | * If only 1 state is defined, that will be the default populated value. 39 | * 40 | * @var array 41 | * @config 42 | */ 43 | private static $allowed_states = []; 44 | 45 | /** 46 | * Define an array of countries that the user can select from. 47 | * If only 1 country is defined, that will be the default populated value. 48 | * 49 | * @var array 50 | * @config 51 | */ 52 | private static $allowed_countries = []; 53 | 54 | /** 55 | * @var string 56 | * @config 57 | */ 58 | private static $postcode_regex = '/^[0-9]+$/'; 59 | 60 | public function __construct() 61 | { 62 | parent::__construct(); 63 | 64 | // Throw exception for deprecated config 65 | if (Config::inst()->get('Addressable', 'set_postcode_regex') || 66 | Config::inst()->get(self::class, 'set_postcode_regex')) { 67 | throw new Exception('Addressable config "set_postcode_regex" is deprecated in favour of using YML config "postcode_regex"'); 68 | } 69 | } 70 | 71 | public function updateCMSFields(FieldList $fields) 72 | { 73 | if ($fields->hasTabSet()) { 74 | $fields->addFieldsToTab('Root.Address', $this->getAddressFields()); 75 | } else { 76 | $newFields = $this->getAddressFields(); 77 | foreach ($newFields as $field) { 78 | $fields->push($field); 79 | } 80 | } 81 | } 82 | 83 | public function updateFrontEndFields(FieldList $fields) 84 | { 85 | $fields->merge($this->getAddressFields()); 86 | } 87 | 88 | public function populateDefaults() 89 | { 90 | $allowedStates = $this->owner->getAllowedStates(); 91 | if (is_array($allowedStates) && 92 | count($allowedStates) === 1) { 93 | $this->owner->State = array_key_first($allowedStates); 94 | } 95 | 96 | $allowedCountries = $this->owner->getAllowedCountries(); 97 | if (is_array($allowedCountries) && 98 | count($allowedCountries) === 1) { 99 | $this->owner->Country = array_key_first($allowedCountries); 100 | } 101 | } 102 | 103 | /** 104 | * @return bool 105 | */ 106 | public function hasAddress() 107 | { 108 | return ( 109 | $this->owner->Address 110 | && $this->owner->Suburb 111 | && $this->owner->State 112 | && $this->owner->Postcode 113 | && $this->owner->Country 114 | ); 115 | } 116 | 117 | /** 118 | * Returns the full address as a simple string. 119 | * 120 | * @return string 121 | */ 122 | public function getFullAddress() 123 | { 124 | $parts = [ 125 | $this->owner->Address, 126 | $this->owner->Suburb, 127 | $this->owner->State, 128 | $this->owner->Postcode, 129 | $this->owner->getCountryName() 130 | ]; 131 | 132 | return implode(', ', array_filter($parts)); 133 | } 134 | 135 | /** 136 | * Returns the full address in a simple HTML template. 137 | * 138 | * @return DBHTMLText 139 | */ 140 | public function getFullAddressHTML() 141 | { 142 | return $this->owner->renderWith('Symbiote/Addressable/Address'); 143 | } 144 | 145 | /** 146 | * Returns a static google map of the address, linking out to the address. 147 | * 148 | * @param int $width (optional) 149 | * @param int $height (optional) 150 | * @param int $scale (optional) 151 | * @return DBHTMLText 152 | */ 153 | public function AddressMap($width = 320, $height = 240, $scale = 1) 154 | { 155 | $data = [ 156 | 'Type' => 'Google', 157 | 'Width' => $width, 158 | 'Height' => $height, 159 | 'Scale' => $scale, 160 | 'Address' => rawurlencode($this->getFullAddress()), 161 | 'Key' => Config::inst()->get(GoogleGeocodeService::class, 'google_api_key') 162 | ]; 163 | 164 | if ($this->owner->hasExtension(Geocodable::class)) { 165 | $data['Lat'] = $this->owner->Lat; 166 | $data['Lng'] = $this->owner->Lng; 167 | 168 | if (is_a($this->owner->getGeocoder(), MapboxGeocodeService::class)) { 169 | $data['Type'] = 'Mapbox'; 170 | $data['Key'] = Config::inst()->get(MapboxGeocodeService::class, 'mapbox_api_key'); 171 | } 172 | } 173 | 174 | $object = $this->owner->customise($data); 175 | return $object->renderWith('Symbiote/Addressable/AddressMap'); 176 | } 177 | 178 | /** 179 | * Returns the country name (not the 2 character code). 180 | * 181 | * @return string 182 | */ 183 | public function getCountryName() 184 | { 185 | return IntlLocales::singleton()->countryName($this->owner->Country); 186 | } 187 | 188 | /** 189 | * Returns TRUE if any of the address fields have changed. 190 | * 191 | * @param int $level 192 | * @return bool 193 | */ 194 | public function isAddressChanged($level = 1) 195 | { 196 | $fields = [ 197 | 'Address', 198 | 'Suburb', 199 | 'State', 200 | 'Postcode', 201 | 'Country' 202 | ]; 203 | $changed = $this->owner->getChangedFields(false, $level); 204 | 205 | foreach ($fields as $field) { 206 | if (array_key_exists($field, $changed)) { 207 | return true; 208 | } 209 | } 210 | 211 | return false; 212 | } 213 | 214 | /** 215 | * NOTE: 216 | * 217 | * This was made private as you should *probably* be using "updateAddressFields" to manipulate 218 | * these fields (if at all). 219 | * 220 | * If this doesn't end up being the case, feel free to make a PR and change this back to "public". 221 | * 222 | * @return array 223 | */ 224 | private function getAddressFields($_params = []) 225 | { 226 | $params = array_merge( 227 | ['includeHeader' => true], 228 | (array) $_params 229 | ); 230 | 231 | $fields = [ 232 | TextField::create('Address', _t('Addressable.ADDRESS', 'Address')), 233 | TextField::create('Suburb', _t('Addressable.SUBURB', 'Suburb')) 234 | ]; 235 | 236 | if ($params['includeHeader']) { 237 | array_unshift( 238 | $fields, 239 | HeaderField::create('AddressHeader', _t('Addressable.ADDRESSHEADER', 'Address')) 240 | ); 241 | } 242 | 243 | 244 | // Get state field 245 | $label = _t('Addressable.STATE', 'State'); 246 | $allowedStates = $this->owner->getAllowedStates(); 247 | if (count($allowedStates) >= 1) { 248 | // If allowed states are restricted, only allow those 249 | $fields[] = DropdownField::create('State', $label, $allowedStates); 250 | } elseif (!$allowedStates) { 251 | // If no allowed states defined, allow the user to type anything 252 | $fields[] = TextField::create('State', $label); 253 | } 254 | 255 | // Get postcode field 256 | $postcode = RegexTextField::create('Postcode', _t('Addressable.POSTCODE', 'Postcode')); 257 | $postcode->setRegex($this->getPostcodeRegex()); 258 | $fields[] = $postcode; 259 | 260 | // Get country field 261 | $fields[] = DropdownField::create( 262 | 'Country', 263 | _t('Addressable.COUNTRY', 'Country'), 264 | $this->owner->getAllowedCountries() 265 | ); 266 | 267 | $this->owner->extend("updateAddressFields", $fields); 268 | 269 | return $fields; 270 | } 271 | 272 | /** 273 | * Get the allowed states for this object 274 | * 275 | * @return array 276 | */ 277 | public function getAllowedStates() 278 | { 279 | // Get states from extending object. (ie. Page, DataObject) 280 | $allowedStates = $this->owner->config()->allowed_states; 281 | if (is_array($allowedStates) && 282 | $allowedStates) { 283 | return $allowedStates; 284 | } 285 | 286 | // Get allowed states global. If there are no specific rules on a Page/DataObject 287 | // fallback to what is configured on this extension 288 | $allowedStates = Config::inst()->get(self::class, 'allowed_states'); 289 | if (is_array($allowedStates) && 290 | $allowedStates) { 291 | return $allowedStates; 292 | } 293 | return []; 294 | } 295 | 296 | /** 297 | * get the allowed countries for this object 298 | * 299 | * @return array 300 | */ 301 | public function getAllowedCountries() 302 | { 303 | // Get allowed_countries from extending object. (ie. Page, DataObject) 304 | $allowedCountries = $this->owner->config()->allowed_countries; 305 | if (is_array($allowedCountries) && 306 | $allowedCountries) { 307 | return $allowedCountries; 308 | } 309 | 310 | // Get allowed countries global. If there are no specific rules on a Page/DataObject 311 | // fallback to what is configured on this extension 312 | $allowedCountries = Config::inst()->get(self::class, 'allowed_countries'); 313 | if (is_array($allowedCountries) && 314 | $allowedCountries) { 315 | return $allowedCountries; 316 | } 317 | 318 | // Finally, fallback to a full list of countries 319 | return IntlLocales::singleton()->config()->get('countries'); 320 | } 321 | 322 | /** 323 | * @return string 324 | */ 325 | private function getPostcodeRegex() 326 | { 327 | // Get postcode regex from extending object. (ie. Page, DataObject) 328 | $regex = $this->owner->config()->postcode_regex; 329 | if ($regex) { 330 | return $regex; 331 | } 332 | 333 | // Get postcode regex global. If there are no specific rules on a Page/DataObject 334 | // fallback to what is configured on this extension 335 | $regex = Config::inst()->get(self::class, 'postcode_regex'); 336 | if ($regex) { 337 | return $regex; 338 | } 339 | 340 | return ''; 341 | } 342 | } 343 | --------------------------------------------------------------------------------