Skip to content

Commit 61da772

Browse files
committed
feat(core): Introduce enhanced model features and refactoring
This commit introduces several new features and significant refactoring to the core location models. * Added an `activated()` scope across all models for consistent active record retrieval, deprecating the older `getActive()` method. * Implemented `scopeByName($term, $locale)` for filtering records by their translated names and a `getName($locale)` helper for retrieving localized names. * Introduced a `scopeNear($lat, $lng, $radius)` method to the City model for proximity-based searches using the Haversine formula. * Refactored common logic for translations, activation, and JSON data loading into new traits: `HasTranslationsAndActivation` and `HasJsonRows`. * The `HasJsonRows` trait now includes caching for JSON data loaded via `getRows()`, improving performance for read-heavy operations. * Updated all documentation files, including README, CHANGELOG, examples, models, performance, and usage guides, to reflect these new features, deprecations, and best practices. * Added initial API resource classes for Country, City, and Area models to facilitate API development.
1 parent 5545884 commit 61da772

File tree

14 files changed

+328
-427
lines changed

14 files changed

+328
-427
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
## v1.4.0
2+
3+
#### Published: 2025-12-24
4+
5+
- [NEW] Added `activated()` scope for filtering by `is_activated` property.
6+
- [NEW] Added `scopeByName()` to filter models by translation in a given locale, with English fallback.
7+
- [NEW] Added `getName()` helper to retrieve model name in a given locale, with English fallback.
8+
- [DEPRECATED] `getActive()` method is now deprecated; use `activated()` scope instead.
9+
- [REFAC] Extracted shared functionality into `HasTranslationsAndActivation` trait (used by Country, City, Area) to reduce code duplication.
10+
- [REFAC] Extracted `getRows()` JSON reading logic into `HasJsonRows` trait with caching and configurable paths.
11+
- [NEW] JSON reading (`getRows()`) now caches results for 1 hour and allows optional config override of file paths.
12+
- [UPDATE] Resources (`CountryResource`, `CityResource`, `AreaResource`) updated to dynamically return names based on requested locale.
13+
- [ENHANCEMENT] `scopeNear()` in City model remains for proximity filtering using Haversine formula.
14+
115
## v1.3.2
216

317
#### Published: 2025-08-29

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
</div>
1515

16-
This package provides a large database with Countries, Cities, Areas, Languages and Currencies models to your Laravel application
16+
Laravel Locations provides a large database with Countries, Cities, Areas, Languages and Currencies models to your Laravel application
1717

1818
The package is ideal for applications that need:
1919

@@ -23,17 +23,19 @@ The package is ideal for applications that need:
2323
- Currency conversion functionality
2424
- Location-based features and filtering
2525

26-
The database contains:
26+
### Database Contents
2727

2828
- 250 Countries
2929
- 5038 Cities (States/Regions)
30-
- 149350 Areas (Cities part of a State/Region)
30+
- 149,350 Areas (subdivisions of Cities/States)
3131

32-
After installation, you can use the Package models to retrieve the data OR directly use the JSON files.
32+
You can access this data either via the **Eloquent models** or directly through the **JSON files** included in the package.
3333

34-
By default, all records are active (the field `is_activated` has a value of 1).
34+
By default, all records are active (`is_activated = 1`). You can filter active records using the **`activated()` scope**.
3535

36-
If you want to exclude certain records, change the field value to 0 and use the model method `getActive()`
36+
> **Deprecated:** `getActive()` method is deprecated since v1.4.0. Use `activated()` scope instead.
37+
38+
---
3739

3840
## Requirements
3941

documentation/examples.md

Lines changed: 32 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,47 @@
1-
#### Address Form with Dependent Dropdowns
1+
# Examples
22

3+
### Country Dropdown for Forms
4+
5+
```php
6+
use Milenmk\Locations\Models\Country;
7+
8+
// Populate a dropdown in English
9+
$countries = Country::activated()->orderBy('translations->EN')->get();
10+
foreach ($countries as $country) {
11+
echo "<option value='{$country->id}'>{$country->getName()}</option>";
12+
}
313
```
4-
<form>
5-
<div class="form-group">
6-
<label for="country">Country</label>
7-
<select id="country" name="country_id" class="form-control">
8-
<option value="">Select Country</option>
9-
@foreach($countries as $id => $name)
10-
<option value="{{ $id }}">{{ $name }}</option>
11-
@endforeach
12-
</select>
13-
</div>
1414

15-
<div class="form-group">
16-
<label for="city">City/State</label>
17-
<select id="city" name="city_id" class="form-control" disabled>
18-
<option value="">Select City</option>
19-
</select>
20-
</div>
15+
### Country → City → Area Dynamic Selects
2116

22-
<div class="form-group">
23-
<label for="area">Area</label>
24-
<select id="area" name="area_id" class="form-control" disabled>
25-
<option value="">Select Area</option>
26-
</select>
27-
</div>
28-
</form>
17+
```php
18+
use Milenmk\Locations\Models\Country;
19+
use Milenmk\Locations\Models\City;
20+
use Milenmk\Locations\Models\Area;
2921

30-
<script>
31-
// JavaScript for dependent dropdowns
32-
document.getElementById('country').addEventListener('change', function() {
33-
const countryId = this.value;
34-
const citySelect = document.getElementById('city');
22+
$country = Country::activated()->first();
23+
$cities = $country->cities()->activated()->get();
3524

36-
if (countryId) {
37-
// Enable city dropdown and fetch cities
38-
citySelect.disabled = false;
25+
$city = $cities->first();
26+
$areas = $city->areas()->activated()->get();
3927

40-
fetch(`/api/cities/${countryId}`)
41-
.then(response => response.json())
42-
.then(data => {
43-
citySelect.innerHTML = '<option value="">Select City</option>';
28+
```
4429

45-
Object.entries(data).forEach(([id, name]) => {
46-
const option = document.createElement('option');
47-
option.value = id;
48-
option.textContent = name;
49-
citySelect.appendChild(option);
50-
});
51-
});
52-
} else {
53-
// Reset and disable dropdowns
54-
citySelect.disabled = true;
55-
citySelect.innerHTML = '<option value="">Select City</option>';
30+
### Proximity Search for Cities
5631

57-
const areaSelect = document.getElementById('area');
58-
areaSelect.disabled = true;
59-
areaSelect.innerHTML = '<option value="">Select Area</option>';
60-
}
61-
});
32+
```php
33+
use Milenmk\Locations\Models\City;
34+
35+
// Latitude/Longitude for Berlin
36+
$nearbyCities = City::activated()->near(52.52, 13.405, 50)->get();
6237

63-
// Similar event listener for city dropdown to load areas
64-
</script>
6538
```
6639

67-
#### Display User's Location with Flag
40+
### JSON Data Usage
6841

69-
```
70-
// In your controller
71-
public function showUserProfile($userId)
72-
{
73-
$user = User::with(['country', 'city', 'area'])->findOrFail($userId);
74-
return view('user.profile', compact('user'));
75-
}
42+
```php
43+
$countries = (new Country)->getRows();
44+
$cities = (new City)->getRows();
45+
$areas = (new Area)->getRows();
7646

77-
// In your view
78-
@if($user->country)
79-
<div class="user-location">
80-
<img src="{{ asset('flags/' . strtolower($user->country->code) . '.svg') }}"
81-
alt="{{ $user->country->name }} flag"
82-
class="country-flag">
83-
{{ $user->country->name }}
84-
@if($user->city)
85-
, {{ $user->city->name }}
86-
@if($user->area)
87-
, {{ $user->area->name }}
88-
@endif
89-
@endif
90-
</div>
91-
@endif
9247
```

documentation/models.md

Lines changed: 25 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,33 @@
1-
```
2-
class Country extends Model
3-
{
4-
protected $fillable = [
5-
'name',
6-
'iso3',
7-
'iso2',
8-
'numeric_code',
9-
'phonecode',
10-
'currency',
11-
'currency_name',
12-
'currency_symbol',
13-
'tld',
14-
'native_name',
15-
'latitude',
16-
'longitude',
17-
'is_activated',
18-
'emoji',
19-
'emojiU',
20-
'translations',
21-
];
22-
23-
protected $casts = [
24-
'translations' => 'array',
25-
'is_activated' => 'boolean',
26-
];
27-
28-
public function cities(): HasMany
29-
{
30-
return $this->hasMany(City::class);
31-
}
32-
33-
/**
34-
* Retrieve all model active records from the database
35-
*/
36-
public function getActive()
37-
{
38-
return self::where('is_activated', 1);
39-
}
40-
41-
/**
42-
* @throws FileNotFoundException
43-
*/
44-
public function getRows()
45-
{
46-
$countryJson = __DIR__ . '/../../database/data/countries.json';
47-
48-
$jsonFileExists = File::exists($countryJson);
49-
if ($jsonFileExists) {
50-
return json_decode(File::get($countryJson), true);
51-
} else {
52-
return [];
53-
}
54-
}
55-
}
56-
```
57-
58-
```
59-
class City extends Model
60-
{
61-
protected $fillable = [
62-
'name',
63-
'country_id',
64-
'city_code',
65-
'latitude',
66-
'longitude',
67-
'is_activated',
68-
];
69-
70-
protected $casts = [
71-
'is_activated' => 'boolean',
72-
];
73-
74-
public function country(): BelongsTo
75-
{
76-
return $this->belongsTo(Country::class);
77-
}
78-
79-
public function areas(): HasMany
80-
{
81-
return $this->hasMany(Area::class);
82-
}
83-
84-
/**
85-
* Retrieve all model active records from the database
86-
*/
87-
public function getActive()
88-
{
89-
return self::where('is_activated', 1);
90-
}
91-
92-
/**
93-
* @throws FileNotFoundException
94-
*/
95-
public function getRows()
96-
{
97-
$cityJson = __DIR__ . '/../../database/data/cities.json';
98-
99-
$jsonFileExists = File::exists($cityJson);
100-
if ($jsonFileExists) {
101-
return json_decode(File::get($cityJson), true);
102-
} else {
103-
return [];
104-
}
105-
}
106-
}
1+
```php
2+
- `id`, `name`, `iso3`, `iso2`, `numeric_code`, `phonecode`, `currency`, `currency_name`, `currency_symbol`, `tld`, `native_name`, `latitude`, `longitude`, `is_activated`, `emoji`, `emojiU`, `translations`,
3+
- `activated()` scope
4+
- `scopeByName($term, $locale = 'EN')`
5+
- `getName($locale = 'EN')`
6+
- `cities()` relation
7+
- `getRows()` JSON reader
1078
```
1089

10+
```php
11+
- `id`, `name`, `country_id`, `city_code`, `latitude`, `longitude`, `is_activated`
12+
- `activated()` scope
13+
- `scopeByName($term, $locale = 'EN')`
14+
- `getName($locale = 'EN')`
15+
- `country()` relation
16+
- `areas()` relation
17+
- `scopeNear($lat, $lng, $radius = 50)` proximity search
18+
- `getRows()` JSON reader
10919
```
110-
class Area extends Model
111-
{
112-
protected $fillable = [
113-
'name',
114-
'city_id',
115-
'country_id',
116-
'latitude',
117-
'longitude',
118-
'is_activated',
119-
];
120-
121-
protected $casts = [
122-
'is_activated' => 'boolean',
123-
];
12420

125-
public function city(): BelongsTo
126-
{
127-
return $this->belongsTo(City::class);
128-
}
129-
130-
public function country(): BelongsTo
131-
{
132-
$this->belongsTo(Country::class);
133-
}
134-
135-
/**
136-
* Retrieve all model active records from the database
137-
*/
138-
public function getActive()
139-
{
140-
return self::where('is_activated', 1);
141-
}
142-
143-
/**
144-
* @throws FileNotFoundException
145-
*/
146-
public function getRows()
147-
{
148-
$areaJson = __DIR__ . '/../../database/data/areas.json';
149-
150-
$jsonFileExists = File::exists($areaJson);
151-
if ($jsonFileExists) {
152-
return json_decode(File::get($areaJson), true);
153-
} else {
154-
return [];
155-
}
156-
}
157-
}
21+
```php
22+
- `id`, `name`, `city_id`, `country_id`, `latitude`, `longitude`, `is_activated`, `translations`
23+
- `activated()` scope
24+
- `scopeByName($term, $locale = 'EN')`
25+
- `getName($locale = 'EN')`
26+
- `city()` relation
27+
- `getRows()` JSON reader
15828
```
15929

160-
```
30+
```php
16131
class Currency extends Model
16232
{
16333
protected $fillable = [
@@ -203,7 +73,7 @@ class Currency extends Model
20373
}
20474
```
20575

206-
```
76+
```php
20777
class Language extends Model
20878
{
20979
protected $fillable = [

0 commit comments

Comments
 (0)