ibexa/core
Ibexa Core is the foundation of the Ibexa Digital Experience Platform, delivering the content repository, public PHP APIs, and MVC layers needed to build and manage enterprise content applications on Symfony.
ibexa/core is not a Laravel package and you should not composer require it into a Laravel app. It is the engine of Ibexa DXP: a Symfony 7.x bundle (Ibexa\Bundle\Core) plus the Public API contracts (Ibexa\Contracts\Core) and their implementations (Ibexa\Core). It arrives as a transitive dependency of the project skeleton, so the real "getting started" is:
composer create-project ibexa/oss my-project
cd my-project && composer require --dev ibexa/behat # optional
php bin/console ibexa:install
php bin/console ibexa:reindex
Stack expectations for 5.x: PHP 8.3/8.4, Symfony 7.4 LTS, MySQL/PostgreSQL, and — for anything beyond basic filtering — Solr or Elasticsearch.
Where to look first, in this order:
vendor/ibexa/core/src/contracts/Repository/ — the services you will inject every day: ContentService, LocationService, SearchService, ContentTypeService, UserService, LanguageService, URLService, FieldTypeService, plus Values/.vendor/ibexa/core/src/lib/ — implementations, FieldType/, Search/ (criterion visitors/handlers), Persistence/.vendor/ibexa/core/src/bundle/Core/Resources/config/ — how services are wired and tagged; this is your map for decoration and extension.First use case — a controller that lists published articles through the Public API (PAPI). This is the shape 80% of your code will take:
use Ibexa\Contracts\Core\Repository\SearchService;
use Ibexa\Contracts\Core\Repository\Values\Content\Query;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion;
final class ArticleListController
{
public function __construct(private readonly SearchService $searchService) {}
public function __invoke(): Response
{
$query = new Query();
$query->filter = new Criterion\LogicalAnd([
new Criterion\ContentTypeIdentifier('article'),
new Criterion\Visibility(Criterion\Visibility::VISIBLE),
]);
$query->sortClauses = [new Query\SortClause\DatePublished(Query::SORT_DESC)];
$query->limit = 20;
$result = $this->searchService->findContent($query);
return $this->render('articles.html.twig', ['items' => $result->searchHits]);
}
}
From a Laravel mental model: PAPI is your Eloquent replacement (but read-mostly and service-oriented), Criterion is the query builder, Twig is Blade, and ibexa.system.<scope> config is your config/*.php.
Inject contracts, never concrete classes. Type-hint Ibexa\Contracts\Core\Repository\ContentService etc. The root Repository object is only for beginTransaction() / commit() / rollback() and for getPermissionResolver(). Everything else goes through the granular services so decoration and test doubles keep working.
Writes: struct in, value out, transaction around it. Every mutation follows *CreateStruct → service call → publishVersion(). Field values must come from the field type's value struct, not arrays:
$contentType = $this->contentTypeService->loadContentTypeByIdentifier('article');
$create = $this->contentService->newContentCreateStruct($contentType, 'eng-GB');
$create->setField('title', 'Hello');
$create->setField('body', $bodyValue); // from FieldTypeService->getFieldType('ezrichtext')->getValueStruct()
$draft = $this->contentService->createContent($create, [$locationCreateStruct]);
$content = $this->contentService->publishVersion($draft->versionInfo);
Wrap multi-object writes (content + relations + locations) in $repository->beginTransaction(); PAPI does not auto-wrap them.
Search: know filter vs. query, and metadata vs. content. $query->filter is cacheable and unscored; $query->query contributes score. Use findContentInfo() when you only need IDs/names, and location queries (LocationQuery) for tree/navigation work. Recent 5.x additions worth knowing: ContentTypeGroupName criterion for findContentTypes (5.0.4), IsBookmarked (4.6.11), and vector/embeddings search (5.0.4) for semantic retrieval alongside keyword search.
Views and Twig. Rendering is view-configuration driven, not controller-per-page. Configure ibexa.content_view rules, then in Twig use ibexa_render(content, { 'view': 'line' }) — the function now accepts parameters (5.0.7). Custom controllers should return ContentView objects so fragment caching and view matchers stay intact.
Siteaccess. Multi-site routing is matcher-driven (HostText, UriText, CompoundMatcher, custom SiteAccessMatcher services). If you store matcher configuration as JSON/YAML, use the SerializerFactory for matchers (5.0.5) rather than hand-rolled normalizers, and keep the CompoundMatcherNormalizer autoconfiguration in serializers.yml.
Events and decoration over patches. React to writes via the repository event classes (Ibexa\Contracts\Core\Repository\Events\...) and cross-cutting concerns via service decoration (ContentService, SearchService, handlers). Avoid touching src/lib internals; decorate at the contract boundary.
IO and binaries. Use IOService with binary create/update structs rather than raw filesystem calls; run php bin/console ibexa:io:migrate-files in batches when moving storages (batching fix in 5.0.5, reference-aware external storage in 5.0.7). Image hashing is deterministic as of 5.0.7, so re-imports stop duplicating files.
Notifications are a first-class API in 4.6.22+ (NotificationService, filtering, mark-as-unread, bulk mark-as-read). Implement TypedNotificationRenderer so notification types get labelled in the UI instead of raw internal names.
Content types as code. Keep content type definitions in YAML migrations rather than clicking them into a database; the admin UI is not a source of truth you can review.
Query::$limit defaults to 10. The most common "missing results" bug. Set it and paginate via $result->totalCount.StringField index length limits (5.0.9), and multilingual empty-option handling for Selection (5.0.8) are all recent. Old minors will bite.StringField get truncated in the index, not in storage — you get a "wrong results only" symptom that is painful to trace. Use the appropriate field type for long content.ForbiddenException is now AccessDeniedHttpException (5.0.4); update tests and error handling. The Content-Length header is also now strictly typed._access_control_attributes is populated on the request (5.0.7) which is handy for logging, but never gate business logic on it.users_group_root_subtree_path (4.6.22) or subtree-based role limitations will match nothing.my-site.example" trap.ibexa_render disagreed before 4.6.22 (IBX-10116). If you decorate rendering, upgrade or your custom renderer is bypassed.NOASSERTION in the metadata — open vendor/ibexa/core/LICENSE and confirm before shipping. Commercial features (cloud, AI embeddings backends) may depend on separately licensed bundles, and 5.x checks for ibexa/cloud.bin/console debug:container 'Ibexa\Contracts\Core\Repository\SearchService', the Symfony profiler's Ibexa data collector (priority-configurable since 4.6.19), bin/console ibexa:reindex after index-affecting changes, and cache:clear plus HTTP-cache purge after writes — reverse-proxy invalidation is not part of the PAPI transaction.ibexa/core is a monorepo read-only split consumed via ibexa/oss and friends; judge it by the release cadence (5.0.x and 4.6.x maintained in parallel) and the issue tracker, not GitHub popularity. Track both branches: 4.6 still receives fixes you may need to backport mentally.How can I help you explore Laravel packages today?