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

Core Persistence Laravel Package

ibexa/core-persistence

Part of Ibexa DXP, this package delivers the core persistence layer: database-backed storage implementations, SPI handlers, and data access abstractions that underpin content, user, and configuration management across the platform.

View on GitHub
Deep Wiki
Context7

Getting Started

ibexa/core-persistence is not a library you install on its own — it is the low-level persistence half of Ibexa DXP, split out of the main monorepo. There is no standalone entry point, no facade, and no meaningful use outside an Ibexa installation.

Minimal steps

  1. Install the platform, not the package:
    composer create-project ibexa/website-skeleton my-project   # or: composer require ibexa/dxp
    cd my-project && composer require ibexa/oss                 # OSS flavour (GPL-2.0 side of the dual license)
    
    ibexa/core-persistence arrives transitively via ibexa/core. Do not add it to require manually — you will end up with a version constraint fighting ibexa/core on every composer update.
  2. Confirm what you actually got:
    composer show ibexa/core-persistence
    ls vendor/ibexa/core-persistence/src
    
  3. Where to look first, in this order:
    • src/contracts/ — the SPI interfaces (Ibexa\Contracts\Core\Persistence\Content\Handler, ...\Content\Location\Handler, ...\Section\Handler, ...\User\Handler, …). These are the stable contracts; everything else is implementation detail.
    • src/lib/Legacy/<Domain>/Handler.php — the concrete handler that implements each contract.
    • src/lib/Legacy/<Domain>/Gateway/DoctrineDatabase.php — where the actual SQL lives.
    • src/lib/Cache/ — the decorating handlers that sit between the API and the legacy layer.
    • src/lib/Doctrine/ — connection/schema plumbing.

First use case

The realistic day-one use is reading content in bulk without paying the full Public API hydration cost: an export job, a sitemap generator, an admin report, or a data-migration pre-check. Inject the SPI handler and pull content plus locations in two queries per batch instead of one repository call per object:

use Ibexa\Contracts\Core\Persistence\Content\Handler as ContentHandler;
use Ibexa\Contracts\Core\Persistence\Content\Location\Handler as LocationHandler;

final class ContentExport
{
    public function __construct(
        private ContentHandler $contents,
        private LocationHandler $locations,
    ) {}

    public function export(array $contentIds): array
    {
        $rows = [];
        // one batched query, keyed by content id
        foreach ($this->contents->loadContentList($contentIds, ['eng-GB']) as $id => $content) {
            $mainLocation = null;
            foreach ($this->locations->loadLocationsByContent($id) as $location) {
                if ($location->main) { $mainLocation = $location; break; }
            }

            $rows[] = [
                'id'        => $content->versionInfo->contentInfo->id,
                'remote_id' => $content->versionInfo->contentInfo->remoteId,
                'name'      => $content->versionInfo->names['eng-GB'] ?? '',
                'path'      => $mainLocation?->pathString,
                'fields'    => array_map(
                    static fn ($field) => $field->value->data,
                    $content->fields
                ),
            ];
        }

        return $rows;
    }
}

Note the shape you get back: Content → versionInfo → contentInfo, and fields is an array keyed by field definition identifier, with values being Content\FieldValue objects (data, externalData, sortKey). That triple indirection is the single biggest source of confusion for newcomers — internalise it before writing anything else.

For anything that writes, use the Public API (Ibexa\Contracts\Core\Repository\ContentService etc.) instead. Reach for this package only when the API is measurably too slow or too coarse.

Implementation Patterns

API-first, SPI-on-exception. The house rule: the Repository/API layer is the public contract (permissions, workflows, cache invalidation, events, signals). Persistence handlers bypass all of it. Restrict SPI usage to read-heavy batch jobs, admin tooling, and custom field type storage. If you find yourself writing content through Content\Handler::create in application code, you are almost certainly creating corrupted state.

Service decoration is the supported extension point. Rather than forking a handler, wrap it:

# config/services.yaml
App\Persistence\LoggingContentHandler:
    decorates: Ibexa\Contracts\Core\Persistence\Content\Handler
    arguments: ['@.inner', '@logger']

Be aware there is already a cache decorator in the chain. Use decoration_priority to decide whether you sit inside the cache (your code sees every call, including cached hits — cheap) or outside it (your code only sees cache misses — you see the real DB traffic). For profiling, you want to be inside; for auditing, outside.

Custom Field Type external storage. This is the package's main legitimate extension surface:

  • A Storage class implementing Ibexa\Contracts\Core\FieldType\StorageGateway (delegates to your gateway).
  • A gateway extending Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter\...-adjacent DoctrineDatabase, targeting your own table.
  • A Converter (Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter) translating FieldValue ⇄ data_text/data_int/data_float columns and, critically, computing sort_key_int / sort_key_string.
  • Registration via the field type service tags (ibexa.field_type, plus the gateway tag — these were ezpublish.* in 2.x/3.x and ibexa.* in 4.x, so copy the tag names out of ibexa/core's Resources/config for your exact version rather than from a blog post).

Bulk / migration work. For content-level changes prefer ibexa/migrations (it drives the API and keeps caches, versions and locations correct). Use raw persistence only for reading legacy data during analysis, or for schema-level fixes that no API exposes.

Reporting and read models. The cleanest pattern is a dedicated read-only service that injects three or four SPI handlers (Content, Location, Section, ContentType) and never touches the entity manager directly. Keep it out of the request/response path — it is built for console commands and queue workers.

Testing. Write integration tests against a real database with fixtures loaded through the installer. Mocking these interfaces is viable (they are plain interfaces) but the mocks rarely behave like the DBAL mappers — especially around the string-vs-int coercion noted below — so reserve mocks for unit-testing your logic, not the data shape.

Gotchas and Tips

  • Licensing. composer show reports NOASSERTION because the package is dual-licensed: Ibexa Business Use License or GPL-2.0. If you are not on an Ibexa DXP subscription you are on GPL-2.0, which is copyleft and will contaminate a proprietary codebase. This is the one gotcha that can sink a project late — settle it before you build on the SPI.
  • Read-only split repo. 3 stars and 0 dependents is not a popularity signal, it is a mirror signal. Issues and PRs belong in the Ibexa monorepo / JIRA; pull requests opened against this repository are generally ignored.
  • Cache staleness. Writing through a gateway or handler that sits below the cache decorator leaves the persistence cache (and any in-request in-memory cache) stale. If you must write low-level, do it inside the transaction wrapper the cache layer exposes, or invalidate explicitly. Symptom: content updates "disappear" until a cache clear.
  • Transactions. Multi-table writes (content row + attributes + tree + URL links) must be atomic. Wrap in the DBAL connection's transaction or the persistence transaction handler; a partially-written content object is not recoverable via the API.
  • ezcontentobject_tree is load-bearing. path_string, main_node_id, parent_node_id and depth must stay mutually consistent. Never UPDATE them by hand — go through Location\Handler (move/copy/hide) or you will orphan subtrees that survive every cache clear.
  • String/int coercion. DBAL returns most numeric columns as strings on MySQL with certain PDO settings; the Mapper classes cast on the way out, but anything you read straight from a gateway returns strings. Type-hint int in your own DTOs and cast explicitly.
  • Versions and status. Content\Handler::load() gives the current published version; drafts are invisible. Use the draft-specific and version-specific methods, and remember Content objects carry a versionInfo whose status is one of DRAFT/PUBLISHED/ARCHIVED (constants on the handler). "Where is my content?" is nearly always a version/status question, not a cache question.
  • Language arguments. Every load method takes an array of language codes. Passing null means "all languages" and returns a fatter object; passing the wrong code returns a Content with an empty fields array and no error. Always verify with $content->versionInfo->names.
  • Sort keys are your responsibility. If you write a custom field type and forget to populate sort_key_int/sort_key_string, location sorting and Field-based queries silently return wrong order — no exception, no warning. sort_key_string is a short VARCHAR, so long text gets truncated; guard for it.
  • Schema changes. The schema derives from the Doctrine mapping in this package plus the installer's tooling; hand-editing tables breaks upgrades. Regenerate with the platform's schema tooling and diff before applying.
  • Debugging queries. Use the Doctrine profiler / bin/console doctrine:query:sql against the ezcontentobject* tables to confirm what the handlers actually produced, and compare with $content->versionInfo->contentInfo. Reading the raw row is usually faster than instrumenting the handler chain.
  • Don't pin it. Version-lock ibexa/core (or the metapackage) only. Pinning ibexa/core-persistence independently guarantees a composer update conflict on the next minor release.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels