danharrin/squire
Squire is a Laravel package for defining an Eloquent model schema in one place and reusing it to generate forms, validation rules, and other UI scaffolding. It keeps field metadata centralized, reducing repetitive boilerplate and helping forms stay consistent across your app.
Squire ships a set of read-only Eloquent models backed by seeded database tables so you don't hand-roll fixture data for countries, currencies, languages, locales and similar reference sets.
composer require squirephp/squirephp artisan squire:install — this publishes the package migrations and config/squire.php, migrates the reference tables, and seeds them.Squire\Models\ and behave like normal Eloquent models:use Squire\Models\Country;
$country = Country::find('gb'); // ISO-3166 alpha-2 keys
$country->currency->code; // 'GBP'
$country->languages; // collection of Language models
First use case to reach for: replacing hardcoded ['GB' => 'United Kingdom', ...] arrays in form selects and factories with Country::orderBy('name')->pluck('name', 'id'), and giving users.country_id a real foreign-key target instead of a free-text column.
Where to look first in the source: config/squire.php (connection + model swapping), then the migrations to see exactly which tables and columns exist before you design your own schema around them.
Swap the model, keep the data. The config exposes a model map so you can point Squire's tables at your own subclasses. This is the clean way to add accessors, scopes, or a HasMany back to your User model without forking the package:
// config/squire.php
'models' => [
\Squire\Models\Country::class => \App\Models\Country::class,
],
class Country extends \Squire\Models\Country
{
public function users(): HasMany { return $this->hasMany(User::class); }
}
Factories and seeders. Reference data is stable, so reference it by ISO code rather than by auto-increment ID — IDs can shift between fresh installs. In factories prefer Country::find('gb')->id or a ->whereIn('id', ['gb','us']) lookup over hardcoded integers.
Validation. Because the tables exist in your DB, exists:countries,id and Rule::exists(Country::class, 'id') work out of the box — no custom in: rule lists to maintain.
Form selects. pluck('name', 'id') on a cached query is usually enough; wrap in Cache::rememberForever() if the select renders on every page. Pair with a ->orderBy('name') scope on your subclass so ordering isn't repeated at every call site.
Localisation of the data itself. If you support multiple languages, join through the language/locale tables rather than storing translated country names in your own table — that's the whole point of pulling this in.
Deployment. Treat the seeded tables as schema, not user data: run migrations in your normal deploy pipeline and run the Squire seeder as part of your standard db:seed (or a dedicated squire:seed step) on every environment, including production, if your app joins against these tables at runtime.
vendor/squirephp/squire/database/migrations against database/migrations and reconcile manually rather than re-running squire:install blindly.squire:install is destructive-ish. Re-running it can overwrite published config and re-seed over existing rows. Use --force deliberately, and never on a database where you've added custom columns to the reference tables by hand.$incrementing = false and $keyType = 'string'. Foreign keys in your own tables must be string/char(2) columns, not unsignedBigInteger.str_getcsv() escape-parameter deprecation in the package's CSV import path. On PHP 8.4 you were getting deprecation noise during seeding on 3.11.0 — upgrading is worth it for that alone. It's a dependency-bump-and-deprecation patch otherwise; no API changes, no config or migration changes to reconcile.Country::find('gb') returns null in tests, it's almost always RefreshDatabase truncating the seeded tables and no seeder running afterwards. Seed in setUp or add the squire seeder to your test case base class.How can I help you explore Laravel packages today?