pdphilip / laravel-data-set
Eloquent style management of data sets in Laravel
Requires
- php: ^8.2
- illuminate/contracts: ^11.0||^12.0||^13.0
- pdphilip/omniterm: ^2
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1
- orchestra/testbench: ^9.0||^10.0||^11.0
- pestphp/pest: ^3||^4
- pestphp/pest-plugin-arch: ^3||^4
- pestphp/pest-plugin-laravel: ^3||^4
- phpstan/extension-installer: ^1.4||^2.0
- phpstan/phpstan-deprecation-rules: ^2
- phpstan/phpstan-phpunit: ^2
README
# Laravel Data Set [](https://packagist.org/packages/pdphilip/laravel-data-set) [](https://github.com/pdphilip/laravel-data-set/actions?query=workflow%3Arun-tests+branch%3Amain) [](https://github.com/pdphilip/laravel-data-set/actions?query=workflow%3Arun-tests+branch%3Amain) [](https://packagist.org/packages/pdphilip/laravel-data-set) Eloquent-style querying for in-memory data sets. No database required.Replace static arrays and config lookups with a queryable, model-like interface. Ideal for reference data (countries, currencies, timezones), test fixtures, and anywhere you need Eloquent ergonomics without a database.
// Define your data set
class CountrySet extends DataSet
{
protected string $modelClass = CountryDataModel::class;
protected function data(): array
{
return [
['id' => 'US', 'name' => 'United States', 'currency' => 'USD', 'dial_code' => '+1'],
['id' => 'GB', 'name' => 'United Kingdom', 'currency' => 'GBP', 'dial_code' => '+44'],
['id' => 'DE', 'name' => 'Germany', 'currency' => 'EUR', 'dial_code' => '+49'],
// ...
];
}
}
// Query it like Eloquent
CountrySet::find('US')->name; // 'United States'
CountrySet::where('currency', 'EUR')->get(); // Collection of European countries
CountrySet::search('united')->pluck('name'); // ['United States', 'United Kingdom', ...]
CountrySet::where('dial_code', '+1')->first()->currency; // 'USD'
Requirements
- PHP 8.2+
- Laravel 10, 11, 12, 13
Installation
composer require pdphilip/laravel-data-set
Quick Start
Inline usage
use PDPhilip\DataSet\DataSet;
$set = new DataSet;
$set->add(['name' => 'Alpha', 'status' => 'active', 'score' => 85]);
$set->add(['name' => 'Beta', 'status' => 'inactive', 'score' => 42]);
$set->add(['name' => 'Charlie', 'status' => 'active', 'score' => 91]);
$set->where('status', 'active')->count(); // 2
$set->where('score', '>', 80)->orderBy('name')->get(); // Alpha, Charlie
$set->search('beta')->first()->score; // 42
Extended data set (recommended)
Create a dedicated class with seeded data:
use PDPhilip\DataSet\DataSet;
class TimezoneSet extends DataSet
{
protected function data(): array
{
return [
['id' => 'UTC', 'name' => 'Coordinated Universal Time', 'offset' => '+00:00'],
['id' => 'EST', 'name' => 'Eastern Standard Time', 'offset' => '-05:00'],
['id' => 'PST', 'name' => 'Pacific Standard Time', 'offset' => '-08:00'],
// ...
];
}
}
// Static facade - data loads once, cached per request
TimezoneSet::find('EST')->name; // 'Eastern Standard Time'
TimezoneSet::count(); // 3
Custom model class
Type your data with a custom model for IDE autocompletion:
use PDPhilip\DataSet\DataModel;
/**
* @property string $id
* @property string $name
* @property string $currency
* @property string $dial_code
*/
class CountryDataModel extends DataModel {}
use PDPhilip\DataSet\DataSet;
class CountrySet extends DataSet
{
protected string $modelClass = CountryDataModel::class;
protected function data(): array
{
return [...];
}
}
Now CountrySet::find('US') returns a CountryDataModel with typed properties.
Relationship-like access
Use a DataSet as a pseudo-relationship on your Eloquent models:
class User extends Model
{
public function country(): ?CountryDataModel
{
return CountrySet::find($this->country_code);
}
}
$user->country()->name; // 'United States'
$user->country()->dial_code; // '+1'
API Reference
CRUD
| Method | Returns | Description |
|---|---|---|
create(array $attributes) | DataModel | Create an unsaved model instance |
add(array $attributes) | DataModel | Create and save a model |
insert(array $rows) | static | Bulk insert rows |
$model->save() | static | Save or update a model |
$model->delete() | void | Remove a model from the set |
// Create without saving
$model = $set->create(['name' => 'Draft']);
$model->status = 'pending';
$model->save();
// Create and save in one step
$model = $set->add(['name' => 'Ready', 'status' => 'active']);
// Bulk insert
$set->insert([
['name' => 'Alpha', 'status' => 'active'],
['name' => 'Beta', 'status' => 'inactive'],
]);
// Modify and re-save
$model = $set->find('us');
$model->name = 'Updated';
$model->save();
// Delete
$model->delete();
Records without an id get a UUID assigned automatically. Auto-generated IDs are hidden from toArray() output, so data round-trips cleanly (load from source, query, modify, export back). The ID is still accessible on the model for
find(), save(), and delete() operations.
To use a custom primary key:
class MySet extends DataSet
{
protected string $primaryKey = 'code';
}
Query Methods
All query methods return a new instance, leaving the original untouched.
| Method | Description |
|---|---|
where(string $key, mixed $operator, mixed $value) | Filter by field. Supports =, !=, <>, <, >, <=, >=, like, ===, !== |
where(string $key, mixed $value) | Shorthand for where($key, '=', $value) |
whereNot(string $key, mixed $value) | Shorthand for where($key, '!=', $value) |
whereStrict(string $key, mixed $value) | Strict === comparison |
whereIn(string $key, array $values) | Filter where field value is in array |
whereNotIn(string $key, array $values) | Filter where field value is not in array |
whereBetween(string $key, array $range) | Filter where field is between [$min, $max] |
whereNotBetween(string $key, array $range) | Filter where field is outside [$min, $max] |
whereNull(string $key) | Filter where field is null or missing |
whereNotNull(string $key) | Filter where field is not null |
search(string $term) | Case-insensitive search across all string fields |
orderBy(string $key, string $direction) | Sort results (asc or desc) |
orderByDesc(string $key) | Sort descending |
groupBy(string $key) | Group results by field (applied on get()) |
limit(int $count) | Limit result count |
offset(int $count) | Skip first N results |
// Chaining
$set->where('status', 'active')
->where('score', '>', 50)
->orderBy('name')
->limit(10)
->get();
// Dot notation for nested data
$set->where('address.city', 'Sydney')->get();
// Array field membership
$set->insert([
['id' => '1', 'tags' => ['php', 'laravel']],
['id' => '2', 'tags' => ['js', 'react']],
]);
$set->where('tags', 'php')->get(); // Row 1
// Group by
$set->groupBy('browser')->get();
// => ['Chrome' => Collection, 'Firefox' => Collection, ...]
// Clone isolation - queries never pollute the base set
$active = $set->where('status', 'active');
$activeHigh = $active->where('score', '>', 80);
$activeLow = $active->where('score', '<', 30);
// $active, $activeHigh, $activeLow are all independent
Terminal Methods
| Method | Returns | Description |
|---|---|---|
get() | Collection | Execute query, return Collection of models |
all() | Collection | All records (ignores filters) |
first() | DataModel\|null | First matching record |
find(mixed $id) | DataModel\|null | Find by primary key |
fetch(string $key, mixed $value) | DataModel\|null | Find first where key equals value |
firstOrCreate(array $attributes, array $values) | DataModel | Find matching or create with merged attributes |
count() | int | Count matching records |
exists() | bool | Any matches? |
pluck(string $value, ?string $key) | Collection | Pluck field values |
toArray() | array | Raw array output (auto-IDs stripped) |
paginate(int $perPage) | LengthAwarePaginator | Paginated results |
update(array $attributes) | int | Bulk update matching rows, returns count |
delete() | int | Bulk delete matching rows, returns count |
// Fetch - shorthand for where()->first()
$set->fetch('email', 'john@example.com');
// First or create
$set->firstOrCreate(
['email' => 'john@example.com'], // search by
['name' => 'John', 'status' => 'active'] // merge if creating
);
// Bulk update
$set->where('status', 'draft')->update(['status' => 'published']); // => 3
// Bulk delete
$set->where('status', 'inactive')->delete(); // => 2
Static Facade
Extended DataSet classes support static method calls. The instance is cached per class for the duration of the request.
CountrySet::where('currency', 'EUR')->get();
CountrySet::find('US');
CountrySet::count();
CountrySet::search('island')->pluck('name');
Use flush() to clear the cached instance (useful in tests or Laravel Octane):
CountrySet::flush();
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.