rainlab/location-plugin
RainLab Location is an October CMS plugin for managing countries and states, with a ready-to-use location database, an address form widget, and backend settings for adding or editing country and state records in your site.
This is not a Laravel package — it's an October CMS plugin (rainlab/location-plugin) that ships with two models, two database tables, and a few form widgets. Composer-installing it into a stock Laravel app won't do anything useful. Skip this assessment if you're not on October CMS.
Install path (October only):
# From the project root, via marketplace
php artisan plugin:install RainLab.Location
# Or drop the folder and migrate + seed
git clone https://github.com/rainlab/location-plugin plugins/rainlab/location
php artisan october:migrate
Where to look first:
models/Country.php and models/State.php — the entire public surface is here (listCountries, getDefault, scopeIsEnabled, formatAddress).updates/ — migrations + seeder for ~250 countries and thousands of states (US, CA, AU, IN, etc.). This is why the plugin exists; nobody wants to seed ISO 3166 by hand.formwidgets/Country.php and formwidgets/State.php — the backend field types you'll reference in fields.yaml.First use case — a backend address form on your own model:
# models/customer/fields.yaml
fields:
country:
label: Country
type: dropdown
# or use the plugin's own widget for the full ISO list + default handling
state:
label: State
type: dropdown
dependsOn: country
Then hydrate in the controller:
public function formExtendFields($widget) {
$widget->getField('country')->options = \RainLab\Location\Models\Country::listCountries();
}
Or reference the built-in form widget directly (type: countrySelect / type: stateSelect) to get dependent state loading for free.
Pattern 1 — Dependent country/state dropdowns in a backend form. The plugin's stateSelect widget reads the parent country field and AJAXes state options. Reference it by alias:
country:
label: Country
type: countrySelect
default: 'US'
state:
label: State/Province
type: stateSelect
dependsOn: country
span: right
Pattern 2 — Frontend partials for public forms. The plugin ships partials you can extend. Typical frontend implementation uses onChange handlers:
{{ form_select('country', Country::listCountries(), input('country')) }}
<select name="state" id="state"
data-request="onLoadStates"
data-request-data="country: '#country'">
</select>
// In a CMS component
public function onLoadStates() {
$countryId = post('country');
return ['#state' => $this->renderPartial('@_states', [
'states' => \RainLab\Location\Models\State::listStates($countryId),
])];
}
Pattern 3 — Store the ISO code, not the row id. Most integrations (including RainLab.User) key off the two-letter code. Country::whereCode('US')->first() is the lookup idiom. Joining to rainlab_location_countries.code is stable across reseeds; joining on id is not guaranteed by anything in the schema.
Pattern 4 — Formatted address rendering. Set the template in Settings → Location → Address Format (e.g. {{ street }}\n{{ city }}, {{ state }} {{ zip }}), then render with the plugin helper so it respects the admin setting:
echo \RainLab\Location\Models\Country::formatAddress($model->toArray());
This is the single most valuable reason to install the plugin if you support multiple regions — the format string lives in admin, not in code.
Pattern 5 — Enable/disable countries per storefront. Use Country::where('is_enabled', true)->get() (or the built-in scopeIsEnabled) and drive your dropdowns from that, so a merchandising decision (hide shipping to X) doesn't need a deploy.
code column semantics differ. Countries use ISO 3166-1 alpha-2 (US, GB). States use a mix of ISO 3166-2 subdivision codes, postal abbreviations, or nothing at all depending on the country. Don't assume state.code == 'CA' means California everywhere; validate against country_id.is_enabled and is_default are separate flags. Setting a default country that is later disabled leaves you with no default. Country::getDefault() returns null in that case — always null-check before using it to prefill a form.formatAddress() needs the full attribute set. It silently drops placeholders whose keys are missing from the array. If your address renders as , , you're passing state_id where the template expects state (a name), or vice versa. Inspect the format string in Settings first.october:fresh, not on october:migrate. In production, updating the plugin can introduce new countries/states but won't remove old ones — plan for orphans when you switch lookups to code-based.type: aliases, dependsOn semantics). Check version.yaml against your core.grimzy/laravel-mysql-spatial or symfony/intl (which gives you Countries::getName('US') and States for free with zero October coupling).Country::extend(function($model){ $model->addDynamicProperty('shipping_zone'); }) in a boot() method in your own plugin rather than forking. Forking makes upstream october:migrate upgrades painful.How can I help you explore Laravel packages today?