dreamfactory/df-core
Core foundation for DreamFactory's Laravel-based API platform, providing shared contracts, utilities, and service infrastructure that power modules for exposing, securing, and managing REST APIs.
dreamfactory/df-core is not a general-purpose Laravel utility — it is the shared foundation of the DreamFactory API platform (the service abstraction, contracts, base models, providers, and utilities that df-system, df-sqlite, connector packages, and the API docs package all build on). That framing determines how you adopt it.
Choose your entry point first:
dreamfactory/df (the platform). df-core arrives transitively; you never touch it directly.df-core supplies the base classes and contracts you extend.Minimal install (extension author):
composer require dreamfactory/df-core
Pin the constraint to the same major/minor line as the platform you are targeting. df-core is released in lockstep with dreamfactory/df; a floating constraint will eventually pull contracts that the host platform does not implement. As of 1.0.18 the aligned platform release is DreamFactory 7.7.1 — if you are on 7.7.0 or earlier, read the 1.0.18 upgrade notes in Gotchas before bumping, because several defaults changed under you.
Read these first, in order:
src/Contracts/ — ServiceInterface, ServiceTypeInterface, cache and script contracts. These define what the platform will call on your code.src/Services/BaseService.php and BaseRestService.php — the lifecycle you inherit: request handling, verb dispatch, event firing, and API doc assembly.src/Providers/ — how service types, routes, and config get registered into the container.src/Models/Service.php — service instances are database rows, not just config entries; this is the single biggest conceptual adjustment for Laravel developers.config/df.php — the platform's config surface, including the serviceTypes map.First use case — a custom service type. Extend the base REST service, implement the verbs you support, and return a documented API surface:
namespace App\Df\Services;
use DreamFactory\Core\Services\BaseRestService;
class InventoryService extends BaseRestService
{
const SERVICE_TYPE = 'inventory';
protected function handleGET($resource, array $params = [])
{
return $this->buildResponse(['sku' => $resource]);
}
protected function handlePOST($resource, array $payload = [], array $params = [])
{
// ...
}
public function getApiDoc()
{
// return a Swagger 2.0 fragment for this service
}
}
Register the type (provider + serviceTypes config), then create an instance through the admin UI or the /api/v2/system/service endpoint. That last step trips people up: the class being registered does not mean an API exists — an instance row must be created and given a role/API key before it responds.
Verb methods, not routes. You do not define Laravel routes. BaseRestService dispatches to handleGET/handlePOST/handlePUT/handlePATCH/handleDELETE/handleMERGE based on the HTTP method, with $resource being the remainder of the path after the service name. This gives you a consistent REST grammar across every connector in the platform and is the main reason to build on df-core rather than a plain controller — your service shows up in the same API docs, role system, and scripting layer as everything else.
Always return the platform response object. Use ServiceResponse (or $this->buildResponse(...)) rather than returning arrays. The platform needs content, content_type, and status_code to apply content negotiation, event hooks, and response caching. Returning a bare array typically produces a 200 with a mangled body and no useful error trail. Since 1.0.18, validation failures raised during an update are reported to the client as 400 rather than the old 500; if you surface validation errors yourself, mirror that convention so clients see one consistent contract.
Use the platform's HTTP wrapper for outbound calls, and know its defaults. Connectors should not reach for raw Guzzle. The df-core curl wrapper is the SSRF-hardened path, and 1.0.18 tightened it:
verify => false.If an existing connector of yours relied on follow-redirects or skipped verification, that is upgrade work, not a bug report.
Filters: write them the way you mean them. The database filter parser was repaired in 1.0.18. Before, unparenthesized a='x' OR b='y' silently matched nothing; now it parses correctly. BETWEEN / NOT BETWEEN are implemented (expanded to range comparisons) rather than being silently unsupported. Two consequences for your code: any filter string you previously "worked around" with extra parentheses or >= / <= pairs can be simplified, and any filter that was quietly returning an empty set may now return rows. Re-run your filter test fixtures after upgrading.
Keep getApiDoc() in sync with behavior. The Swagger fragment drives the generated /api/v2/api_docs output, the admin UI's interactive docs, and (in some deployments) request/response validation. Treat it as code: if you add a query parameter in handleGET, add it to the doc in the same commit. Mismatches are silent until a consumer generates a broken SDK.
Know what your roles can be granted. The service access list is the vocabulary your role definitions are written against. _spec is now listed as a grantable operation (previously it existed but could not be assigned to a role), so if you expose a spec endpoint, document it as a permission rather than assuming it inherits the table/verb grants.
Consume services internally through the service manager. If you need to call another DreamFactory service from your own (fan-out/aggregation), resolve it via the service manager rather than issuing HTTP requests back to yourself. You inherit the current user's session, role restrictions, and event context, and you avoid a pointless network hop:
$svc = $this->serviceManager->getService('mysql', $request->getApiKey());
$result = $svc->handleRequest($request, 'GET', 'customers', $params);
Layer caching per service instance, not globally. df-core exposes cache contracts and traits used by connectors. Namespace keys by service id plus resource path plus a hash of params — DreamFactory is multi-tenant and multi-role by design, and a cache key that ignores the role or API key will leak data across tenants. Cache the expensive upstream call, not the formatted response, when role-based field filtering applies.
Treat the event system as your AOP layer. Service lifecycle events fire around request handling. Cross-cutting concerns — audit logging, per-tenant rate limiting, response scrubbing, timing metrics — belong in listeners registered from your own service provider, not duplicated into every connector:
Event::listen('df.service.pre_process', function ($event) { /* ... */ });
Custom scripting engines. If your use case is "let customers write small transformations," implement the script engine contract rather than inventing a mini-language layered on top of the service. The platform's scripting API already wires your service's data into the script context and back.
If you ship MCP query templates, use the platform's argument shape. 1.0.18 changed template arguments to snake_case and moved group counts onto aggregate_data. Templates authored against 1.0.17 will render with empty or undefined arguments after the upgrade — treat template definitions as part of your service's public surface and version them alongside the package.
Package it as a Composer package. Custom service types belong in a package with its own service provider, not in app/. The platform's extension model expects to find providers and asset/config publishing in a package, which also makes version pinning and rollout across environments tractable.
df-core assumes the platform's database schema, migrations, config keys, and other df-* packages. Pulling it into an ordinary Laravel app gives you base classes that reference tables and config that do not exist. If you are not inside a DreamFactory installation, this package is the wrong dependency.df-core and the platform share contracts that change between releases. Match versions exactly, and treat any platform upgrade as requiring a compatibility run of your custom services — the failures often surface at runtime in handleRequest, not at composer update. 1.0.18 pairs with DreamFactory 7.7.1.500 for update validation failures; those are now 400.a='x' OR b='y' used to match nothing; it now matches correctly. If downstream code "worked" because that filter returned zero rows, it will now process real data — and possibly more than the role was expected to see. Re-run your role/permission tests against real datasets, not fixtures.serviceTypes configuration, and an instance must exist as a database row. Missing any one produces the same unhelpful symptom: the type is invisible in the admin UI.php artisan about / the loaded providers list when behavior seems doubled.getApiDoc() breaks more than your service. The API docs endpoint aggregates all services; one malformed Swagger fragment can take down the entire docs page and, in stricter setups, fail API generation for unrelated services. Guard against unset config and validate the fragment shape.BaseRestService gives you dispatch, not authorization. Role and API-key restrictions are applied by the platform around the standard service flow, but any custom resource paths or admin-only verbs you add need explicit checks. Assume a request that reaches your handler has already passed platform-level checks — and only those.content_type and platform config. If clients report "the JSON has an extra envelope," check the response object and config before rewriting your handler. ?model=true metadata is now built from the caller's actual _table listing, so model responses are more accurate but may differ in shape from what a client cached.description is always a non-empty string.APP_DEBUG in the environment, watch the platform's log channel, and use the platform's artisan commands (service listing, config inspection) before adding dd(). Custom service exceptions are caught and converted by the platform, so raw stack traces rarely reach the client — log the exception and include a stable error code/message pair in your response so support can correlate. For outbound HTTP failures post-1.0.18, log the resolved IP, whether the redirect was opted in, and the TLS verification outcome; "connection failed" is otherwise indistinguishable between an SSRF block and a bad certificate.src/Contracts and BaseRestService are short and worth reading end to end), the dreamfactory/df repository for working examples of every service type, the release PRs on GitHub (1.0.18's six fix PRs document real-world failure modes better than any guide), and the DreamFactory docs site. Budget time for reading reference implementations of connectors — that is faster than searching.AND/OR, BETWEEN) and for role service-access after package import — both are areas where 1.0.18 changed observable behavior.How can I help you explore Laravel packages today?