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

Eloquent Filemaker Laravel Package

bluefeather/eloquent-filemaker

Work with FileMaker databases through the Data API using familiar Laravel Eloquent models and query builder. Supports relationships, portals, containers, field remapping, script running, and automatic authentication and session management across multiple files and credentials.

View on GitHub
Deep Wiki
Context7

Getting Started

Server-side prerequisites first. This package is only as good as the layout you point it at. Before writing PHP, confirm: FileMaker Server 17+ (or Claris FileMaker Cloud) with the Data API enabled, a dedicated account whose privilege set has the fmrest extended privilege enabled, a valid TLS certificate (self-signed certs require ssl_verifypeer => false), and a layout that actually contains every field you intend to read or write. Fields not on the layout are invisible to the API — this is the single biggest source of "why is my attribute null".

Install and publish config:

composer require gearbox-solutions/eloquent-filemaker
php artisan vendor:publish --provider="GearboxSolutions\EloquentFileMaker\FileMakerServiceProvider" --tag=config

Point it at your file (.env plus config/filemaker.php):

FM_HOST=fm.example.com
FM_DATABASE=MyApp
FM_USERNAME=web_api
FM_PASSWORD=secret
// config/filemaker.php
'connections' => [
    'filemaker' => [
        'driver'   => 'filemaker',
        'host'     => env('FM_HOST'),
        'database' => env('FM_DATABASE'),
        'username' => env('FM_USERNAME'),
        'password' => env('FM_PASSWORD'),
        'protocol' => 'https',
        'port'     => 443,
        'version'  => 'vLatest', // match your server: v1 / v2 / vLatest
    ],
],

Keep MySQL as your default connection and reference FileMaker by name; do not make FileMaker the app-wide default.

First model — treat the layout name as the table:

namespace App\Models\FileMaker;

use GearboxSolutions\EloquentFileMaker\Model;

class Contact extends Model
{
    protected $connection = 'filemaker';
    protected $layout     = 'Contacts';   // a layout, not a table
    protected $primaryKey = 'id';         // a field present on the layout
    public $timestamps    = false;        // FileMaker won't populate these
    protected $fillable   = ['first_name', 'last_name', 'email', 'status'];
}

First query:

$contacts = Contact::where('status', 'Active')
    ->orderBy('last_name')
    ->limit(50)
    ->get();

First use case to build: a read-only reporting or integration surface over an existing FileMaker solution — a dashboard, a customer lookup, or a webhook-driven sync. Read paths are where this package shines and where you can validate the layout/field assumptions cheaply before touching writes.

Where to look first in the source: the base Model class and the FileMakerQueryBuilder (to see which Eloquent methods are actually translated into the Data API's _query/_sort/_limit/_offset parameters), then the connection class for token/session handling.

Implementation Patterns

Keep FileMaker models at the edge of your app. Wrap them in repositories or dedicated services and return plain arrays/DTOs to controllers. FileMaker's query semantics, pagination, and error model are different enough from SQL that leaking Contact::where(...) into business logic will bite you later. It also makes the eventual "cache/MySQL mirror" decision a one-file change.

Lean on FileMaker find operators instead of SQL constructs — they pass through the where() values:

Contact::where('created', '1/1/2024...12/31/2024')->get();  // range
Contact::where('company', '*smith')->get();                 // wildcard
Contact::where('code', '==ABC')->first();                   // exact match
Contact::where('status', 'Active')->where('city', 'Boston')->get(); // AND within one request

Paginate explicitly. Don't rely on Model::all(). Use limit()/offset() and always pair with an orderBy() so the found set is stable across requests:

$page = Contact::where('status', 'Active')
    ->orderBy('last_name')
    ->orderBy('id')
    ->limit(100)->offset($page * 100)
    ->get();

For bulk exports, loop offsets in a queued job rather than trying to stream a whole found set in one HTTP request. paginate() may work via the Data API's foundCount, but explicit limit/offset is the pattern you can reason about.

Writes are recordId-driven. create() returns a model with the FileMaker recordId attached; subsequent save()/delete() calls PATCH/DELETE that specific record:

$contact = Contact::create(['first_name' => 'Jane', 'email' => '[email protected]']);

$contact->email = '[email protected]';
$contact->save();     // PATCH by recordId

$contact->delete();   // DELETE by recordId

Never reconstruct a model from session data or form input and call save() — re-fetch it. The primary key field and the internal recordId are not the same thing, and updates/deletes depend on the latter.

Use portals for related data. Portals come back with the parent record in one round trip, which is exactly what you want given the HTTP cost:

class Contact extends Model
{
    public function invoices()
    {
        return $this->hasMany(Invoice::class, 'contact_id', 'id')
                    ->portal('Invoices'); // portal object name on the layout
    }
}

Fetch the parent once, then read $contact->invoices. Verify eager loading (with('invoices')) in your version — if it issues a separate query per parent, you've turned one HTTP call into N.

Let FileMaker run its scripts. If the solution has business logic (auto-enter, validation, related-record creation), invoke it rather than reimplementing it:

$contact = Contact::where('id', $id)->first();
$contact->runScript('Sync to Billing', '{"reason":"web"}');

Prefer post-commit phases for side effects like emails or external calls, and remember the script runs with the API account's privileges, not the end user's.

Multiple files / multiple connections are just more entries in the connections array, with protected $connection = 'filemaker_billing' on the relevant models.

Escape hatch for one-offs: DB::connection('filemaker')->table('Contacts')->where('status', 'Active')->get() gives you the builder without a model when you need a quick probe or a health check.

Container fields (images, documents) come back as URLs that require the session token to fetch. Don't put them straight into <img src>; proxy them through a route in your app that streams the bytes.

Sync pattern for heavy workloads. Schedule a job that pulls changed records (using a modified timestamp range find) into a MySQL table with proper indexes. Do reporting and joins there; keep FileMaker as the system of record. This is the standard escape from "every query is an HTTPS round trip".

Testing: because the Data API is HTTP, Http::fake() (or a Guzzle mock, depending on the version's client) covers unit tests well. Keep a real staging FileMaker file for integration tests — mocked payloads won't catch a field that was removed from a layout.

Gotchas and Tips

  • No transactions, joins, aggregates, or migrations. DB::transaction() gives false confidence: the Data API has no transactional semantics across requests. If you need atomic multi-record work, put it in a FileMaker script and call it once. There is no schema builder — the layout is your schema.
  • Layout drift is silent. Remove a field from a layout and Eloquent returns null instead of erroring. Add a smoke test per model that asserts expected attributes exist, and re-run it after any FileMaker deployment.
  • find() is not find() in SQL. The Data API's record lookup is by recordId, not your business key. Safest habit: Model::where($primaryKey, $id)->first() for business-key lookups, and use the recordId only for update/delete.
  • Compound find semantics ≠ SQL boolean logic. The _query array ANDs fields within one entry and ORs entries together. Chained orWhere() calls can group differently than you expect, and there's no equivalent of omit in SQL. Log the generated payload when a find returns an empty set unexpectedly.
  • Partial support for whereNull, whereIn, whereHas, subqueries, whereRaw. Validate each one against your FileMaker version. For small result sets, fetching and filtering in PHP is often the pragmatic answer.
  • Dates and numbers arrive as strings. FileMaker dates are locale-formatted (MM/DD/YYYY by default in the API), not ISO. Declare $casts and be deliberate about the date format on write, or you'll get off-by-a-day bugs and timezone drift. Numeric fields can also come back as strings depending on the field type.
  • $fillable matters more than usual. Every field on the layout comes back in the response, so guard against mass-assignment surprises on both the read and write side, and use $hidden for internal fields.
  • Field names with spaces or punctuation need care: use $model->{'Field Name'} for access and keep $fillable keys byte-for-byte identical.
  • Session tokens expire (~15 minutes idle). Let the package re-authenticate rather than caching tokens yourself in a long-running queue worker, and watch FileMaker Server's concurrent Data API connection limit — a busy queue can exhaust it.
  • Errors are HTTP 500-class with FileMaker error codes in the body. Expect: 401 "no records match the request" (a find miss, not auth), 105 "layout is missing or not accessible", 212 invalid credentials, 952 invalid Data API token, and validation failures returning FileMaker's own message text. Wrap calls and translate these into your app's error model instead of letting raw HTTP exceptions escape.
  • Every query is a network round trip. N+1 across portals will destroy a page's response time. Batch by portals, cache aggressively, and never call Model::all() on a production layout.
  • toSql() is meaningless here. For debugging, log the outgoing request payload (enable HTTP/Guzzle logging or a middleware) and inspect the _query, _sort, and _offset values. That's where the real bugs live.
  • Sorting before paging is mandatory. FileMaker's found-set order is not guaranteed without an explicit sort, so offset paging without orderBy() will duplicate and skip records.
  • Extension points: extend the package's Model base with your own abstract class to add shared scopes (e.g. scopeActive, a whereDateRange helper that emits FM range syntax) rather than repeating find-syntax strings across models. If you need custom transport behavior, swap or extend the connection class in config.
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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel