jsdecena/mcpro
MCPro is a Laravel/PHP package by jsdecena that provides a lightweight, configurable toolkit for PHP applications. It installs via Composer and is designed for straightforward integration into Laravel projects, giving developers a focused set of utilities without heavy dependencies.
composer require jsdecena/mcpro
Publish the package assets first — most Laravel geography packages ship migrations, seeders, and sometimes a config file in vendor/:
php artisan vendor:publish --provider="Jsdecena\Mcpro\McproServiceProvider"
Then migrate and seed. The seed step is the critical one: this package ships the PSGC (Philippine Standard Geographic Code) dataset, so expect a large insert run:
php artisan migrate
php artisan db:seed --class="Jsdecena\Mcpro\Database\Seeders\McproSeeder"
Where to look first
vendor/jsdecena/mcpro/src/Models/ — this is where you'll see the exact class names (Province, City, Municipality, Barangay — the naming convention varies and determines the rest of your integration).vendor/jsdecena/mcpro/database/migrations/ to confirm table names and column names (code, name, province_code, city_code, etc.). Everything downstream — your FKs, your validation rules — depends on these strings.First use case: an address dropdown
The canonical day-one task is a cascading province → city/municipality selector on an address form. If models are exposed, it looks like:
use Jsdecena\Mcpro\Models\Province;
use Jsdecena\Mcpro\Models\City;
$provinces = Province::orderBy('name')->get();
$cities = City::where('province_code', $request->province_code)
->orderBy('name')
->get();
Render those into <select> elements, wire an Alpine/Livewire x-on:change listener, and you're done. If the package doesn't expose scopes, you may need to filter with ->where(...) directly on the model — nothing else should be required.
Don't try to store denormalized address strings on the mcpro tables — they're read-only reference data. Add FK columns to your tables:
Schema::table('customers', function (Blueprint $table) {
$table->foreignId('province_id')->nullable()->constrained('provinces');
$table->foreignId('city_id')->nullable()->constrained('cities');
$table->string('barangay')->nullable(); // string, since barangays rarely need FKs
});
Then define belongsTo relationships in your Customer model pointing at the mcpro models. If the package doesn't define inverse hasMany relations (many geography packages don't), add them via a local subclass or an Addressable trait you own — do not edit vendor files.
Two patterns work well:
Livewire — bind $province_code, then in updatedProvinceCode() reset $city_code and reload $cities. This is the cleanest for admin forms.
Alpine + JSON endpoint — ship a single GET /api/geo/cities?province={code} controller that returns City::where('province_code', $code)->select('code','name')->get(). Cache the response because it's immutable reference data.
public function cities(string $province)
{
return Cache::rememberForever("geo.cities.{$province}", fn () =>
City::where('province_code', $province)->orderBy('name')->get(['code','name'])
);
}
Never trust the FK chain in a request. Validate each level against the parent:
$request->validate([
'province_code' => ['required', 'exists:provinces,code'],
'city_code' => [
'required',
Rule::exists('cities', 'code')->where('province_code', $request->province_code),
],
]);
That Rule::exists()->where(...) guard is the single highest-value pattern with this package — it stops a malicious payload from submitting an address where the city doesn't belong to the province.
The dataset is static reference data, not user data.
DatabaseSeeder and use RefreshDatabase in tests as usual.php artisan geo:seed command you wrap around the package seeder, guarded by an if (Province::count() === 0) check. Don't put it in your regular DatabaseSeeder, or you'll re-run it every deploy.->insert() in one shot, wrap it as DB::transaction() and check max_execution_time. On managed MySQL (PlanetScale, RDS) you may need to split by region.For "type your city" UX, skip the province cascade. Add a plain LIKE scope in a repo class:
City::where('name', 'like', "%{$q}%")->limit(20)->get();
If you need this fast, add an index on cities.name (the package likely only indexes code). If you need "Davao" to match "Davao City" and "Davao del Sur", that index alone may not cut it — consider pg_trgm (PostgreSQL) or full-text.
Table names may not match the package name. mcpro is not obviously the prefix. Check migrations before writing FKs; you may end up with provinces, cities, and a surprise municipalities table (in PSGC, cities and municipalities are technically the same level — the package may model them as one table with a type column, or two tables). Verify before writing a schema.
No independent-city handling at the FK level. In the Philippines, chartered cities (e.g. Cebu City, Davao City) are administratively independent of their province even though PSGC often still lists a province code. If your app bills or delivers, don't assume city->province_id is always populated or accurate. Nullable province FKs on your side are safer than assuming the package got this right.
Naming collisions. If you already have App\Models\City (generic, non-PH), importing Jsdecena\Mcpro\Models\City will collide in service classes. Alias at the use statement: use Jsdecena\Mcpro\Models\City as PhCity;. Don't rename the vendor table — the seeder will break.
Re-seeding is destructive. Truncating provinces/cities while your customers.province_id FKs point at them will fail on SET NULL/RESTRICT depending on your migration. Before upgrading the package (and its seeder), snapshot your FKs and re-map any codes that changed between PSGC releases. PSGC codes do change when municipalities are reclassified.
Migrations vs. your migrations. Publishing vendor migrations into database/migrations/ timestamps them at the publish moment. If you already have tables named provinces or cities, you get a hard collision. Check whether the package uses Schema::hasTable() guards — if not, run php artisan migrate --pretend first on a staging DB.
Seeding timeout. The PSGC dataset has ~1,600+ cities/municipalities and ~42,000 barangays. If barangays are included, the seeder will spin for minutes on a cheap DB. Bump max_execution_time or run inside a queued job. On SQLite for tests, this is usually fine — on shared MySQL it isn't.
Case sensitivity. PHP strtoupper/strtolower comparisons on city names can break on non-ASCII strings (e.g. "Niño Aquino", "Peñablanca"). Use mb_strtolower or compare codes instead of names in business logic. Codes are the stable identity.
Caching invalidation. If you Cache::rememberForever province/city lists, a package upgrade that changes PSGC codes silently leaves stale entries. Either key your cache by package version (geo.v{$version}.provinces) or run php artisan cache:clear as part of the upgrade runbook.
Extension points. The package almost certainly doesn't ship a config file, so if you need to change model behavior (add a displayName accessor, override fillable), extend rather than modify:
namespace App\Models;
class Province extends \Jsdecena\Mcpro\Models\Province
{
public function getDisplayNameAttribute(): string
{
return "{$this->name} ({$this->code})";
}
}
Then point your FKs and relations at App\Models\Province. Just remember the seeder writes to the vendor table, so subclassing only changes behavior, not stored data.
Don't rely on relations being eager-loaded by default. Many small packages omit protected $with. If you render 100 cities in a table with $city->province->name, add City::with('province') explicitly — otherwise you'll get N+1 queries in production.
Testing. Reference-data packages are usually skipped from factories. For tests, either seed the package seeder in RefreshDatabase or use Model::unguarded(fn () => Province::insert([...])) with minimal fixtures. The latter is much faster and keeps tests independent of PSGC release drift.
How can I help you explore Laravel packages today?