Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Datasource Laravel Package

fsi/datasource

FSI DataSource is a Laravel/PHP package for building reusable data sources behind tables and APIs. It offers a structured way to define queries, apply filters and sorting, handle pagination, and transform results, keeping controllers clean and consistent.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and pick your entry point. composer require fsi/datasource installs the component only. In practice most applications pull it in transitively via fsi/datasource-bundle (Symfony DI wiring, tagged drivers, request binding). Check your composer.lock first — with 0 dependents on Packagist, the component is almost always consumed through the bundle in a legacy Symfony app.
  2. Read in this order: README.md → docs/ (driver configuration, field types, extensions) → src/DataSource.php → src/Driver/Collection/CollectionDriver.php. The Collection driver is the smallest complete reference implementation; read it before the Doctrine ORM driver.
  3. Learn the mental model before writing code. A DataSourceFactory holds registered drivers. createDataSource($driverName, $options, $name) hands off to a driver, which builds a DataSource — a container of Field objects plus bound parameters. The DataSource itself never queries anything; getResult() delegates to the driver, which translates fields + comparisons into a query. Fields are declarative descriptors: name, type (text, number, date, boolean, entity, …), allowed comparisons (eq, like, gt, between, …), and the underlying mapping (field).
  4. First use case — a filterable, paginated list. Start with the Collection driver (an array in memory, no DB), confirm the bind → result → pager loop, then swap in the Doctrine ORM driver; the DataSource API is identical.
use FSi\Component\DataSource\DataSourceFactory;
use FSi\Component\DataSource\Driver\Collection\CollectionDriver;
use Symfony\Component\EventDispatcher\EventDispatcher;

$factory = new DataSourceFactory(
    [new CollectionDriver()],
    new EventDispatcher()          // Core extension (pagination + ordering) is registered by the factory
);

$dataSource = $factory->createDataSource('collection', [
    'collection' => $users,        // array or Traversable
    'fields' => [
        'name'  => ['type' => 'text',   'comparison' => 'like'],
        'email' => ['type' => 'text',   'comparison' => ['eq', 'like']],
        'age'   => ['type' => 'number', 'comparison' => 'gte'],
    ],
], 'users');

$dataSource->bindParameters([
    'name' => ['comparison' => 'like', 'value' => 'an'],
]);

$dataSource->getPager()->setMaxResults(20);
$dataSource->getPager()->setCurrentPage(2);

$rows  = $dataSource->getResult();   // array of records
$pager = $dataSource->getPager();    // getTotalResults(), getPageCount(), getCurrentPage()…

Once that loop is green, the production version is the same shape with 'doctrine-orm' and 'entity' => User::class in the options array.

Implementation Patterns

Factory as a singleton in the container. Build the factory once (or let the bundle's fsi_data_source.factory service do it) and inject it. Registering drivers per-request is wasted work; drivers are stateless and cheap to share.

Field maps are configuration, not code. Centralise field definitions in a method or config array per resource, and keep the comparison list explicit. Whitelisting comparisons (['eq', 'like', 'between']) is how you control which operators the UI is allowed to submit — the field rejects anything else.

Bind from the request, then render a view. In Symfony, bindParameters($request->query->all()) (the query bag, not the whole request) followed by createView(), which gives you a form-like structure of filter widgets plus the Core extension's ordering/pagination controls. Render and validate through the view rather than hand-building a filter form.

Pagination is two numbers, not a LIMIT. The driver sets first/max results on the underlying query and then runs a separate count to populate Pager::getTotalResults() / getPageCount(). Set maxResults once per list, and read the current page from the pager after binding so page links survive filtering.

Customise the ORM query with the qb closure. The Doctrine driver accepts a query-builder callback in its options. Use it to add joins, enforce soft-delete / tenancy / ACL conditions, and set distinct() — that's where cross-cutting list scoping belongs, not in the controller.

Extensions and event subscribers for cross-cutting behaviour. The component dispatches events around binding and result fetching (PRE_BIND_PARAMETERS, POST_BIND_PARAMETERS, PRE_GET_RESULT, POST_GET_RESULT, and field-level equivalents). A custom extension subscribing to PRE_GET_RESULT is the idiomatic hook for "always filter by the current tenant" or "default sort by created_at desc". Register it on the data source after creation.

One data source per list, named. When a page hosts several lists, give each a distinct name and namespace bound parameters by that name (the bundle does this for you) so filters don't leak between tables.

Custom drivers. Implement DriverInterface (or extend the abstract driver), return a configured DataSource from getDataSource(), and translate bound fields into your own query language. Copy CollectionDriver and replace the in-memory filtering with the real back end. Drivers for Doctrine ORM, Doctrine DBAL, Elastica, and Collection ship in-tree.

Test with the Collection driver. Unit-test field/comparison logic against an array — no database, no mocks — then add one integration test per real driver against a seeded fixture to catch mapping drift.

Gotchas and Tips

  • It is effectively unmaintained. Last tagged release is February 2021 (0 dependents, 8 stars). Check composer.json for the PHP and symfony/event-dispatcher constraints before adopting; on PHP 8.1+ you may need --ignore-platform-reqs, a fork, or a patch for return types and deprecations. Budget for that up front.
  • The parameter array shape is the #1 source of "why is my filter ignored?" bindParameters() expects fieldName => ['value' => …, 'comparison' => …], nested under the data source name when namespaced by the bundle. Malformed or mis-nested input is silently dropped: $dataSource->getParameters() after binding is your first debugging call.
  • Comparison must be in the field's allowed list — otherwise the value is rejected or ignored rather than throwing. If a filter "works for eq but not like", the field definition is the culprit, not the driver.
  • Count queries are the hidden cost. A separate SELECT COUNT(*) runs per fetch, and joins/distinct can make it far more expensive than the page query. For heavy lists, constrain the count via the qb callback and watch the DQL in the profiler.
  • The Collection driver is in-memory. Pagination and filtering happen in PHP after loading the whole collection — fine for tests and small sets, a footgun for real data.
  • Decouple field names from property names. Use the field option to map a UI-friendly field name onto the entity property; renaming an entity column should not break URLs or saved filters.
  • Standalone wiring needs an event dispatcher and a form factory. createView() builds Symfony Form objects, so outside the bundle you must supply both. If you get "no form factory" or missing-widget errors, you skipped this.
  • Ordering parameter names are Core-extension owned. Dump createView() to see the exact keys it registers (_sort/_order-style) rather than guessing; they're also the fastest way to confirm the extension is actually loaded.
  • Pager access differs between minor versions. Grep src/ for function getPager and confirm whether it's on the DataSource or reached via the pagination extension before writing controller code against it.
  • Debug in this order: bound parameters → generated DQL/query (getDQL() on the ORM driver's query builder) → result count → pager totals. Ninety percent of "the filter does nothing" bugs are resolved at step one.
  • Don't try to force it into a new project. For greenfield Symfony, Doctrine's own Paginator plus a FormType for filters is less code, maintained, and does not require an event-dispatcher-era dependency. Use this package only where it already exists.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console