netgen/ezplatform-site-api
Netgen Site API offers a developer-friendly abstraction over the eZ Platform/Ibexa content repository, simplifying content and location queries, field access, and template rendering. It reduces boilerplate and speeds development of websites and APIs.
composer require netgen/ezplatform-site-api for eZ Platform 2.x. If you are on Ibexa 3.3+ (this repo is the older, renamed lineage), use netgen/ibexa-site-api instead — the namespaces become Netgen\IbexaSiteApi\*.Netgen\EzPlatformSiteApi\EzPlatformSiteApiBundle in app/AppKernel.php (or config/bundles.php on Flex). The bundle wires the Site and Finder services plus the Twig extension.config/packages/ng_site_api.yaml (or app/config/config.yml) with at least content_view / location_view entries once you start rendering. Everything else works out of the box; the site and finder services are auto-registered.Netgen\EzPlatformSiteApi\API\Site (entry point for loading content/locations), Netgen\EzPlatformSiteApi\API\Finder (the SQL query builder), Netgen\EzPlatformSiteApi\API\Query (the fluent query DSL), and the Twig extension (ng_view, ng_image, ng_field_*).$contentService->loadContent($id) with the Site API:
use Netgen\EzPlatformSiteApi\API\Site;
public function show(Site $site, int $id)
{
$content = $site->findContent($id); // Content API object
$location = $site->findLocation($id);
$root = $site->getRootLocation(); // siteaccess-aware
}
The returned Content/Location objects expose typed ->fields and ->innerContent (raw eZ object) — most controller code simply stops calling the repository.Finder-first list pages. The Finder is the reason most teams adopt this bundle: it compiles a Query into a single SQL statement instead of the legacy search stack.
use Netgen\EzPlatformSiteApi\API\Query;
$query = Query::create()
->withContentType('article')
->withParentLocation($location->id) // or withParentLocationId($id)
->withIsVisible(true)
->withIsMainLocation(true)
->sortBy('published', Query::SORT_DESC)
->slice($offset, $limit);
$items = $finder->findContent($query); // array of Content
$one = $finder->findOneContent($query); // Content|null
$total = $finder->countContent($query); // int
findLocations(), findOneLocation(), countLocations() are the equivalents for location results.
Build reusable queries. Extract a Query factory (a small service or static method on a domain class) and inject the Finder. This keeps controllers thin and lets you unit-test query criteria. Filter helpers include withContentType, withContentTypeGroup, withSection, withField, withFieldRelation, withRemoteIdIn, withContentIdIn, withLocationIdIn, withIsVisible, withIsMainLocation, withLanguages, and sortBy overloads for content fields, dates and location priority.
Controllers render through the view layer. Instead of hand-wiring templates, name a view:
{{ ng_view(content, 'full') }} {# preferred #}
{{ ng_render(ng_view(content, 'line')) }} {# older alias, still valid #}
Configure the mapping once in ng_site_api.content_view / location_view using match value types (Id\ContentType, Id\Section, Id\Content, Id\Location, Id\ParentContentType, etc.). Views are matched in insertion order, so put the most specific match blocks first.
Fields and images. Field access is lazy and typed:
{{ content.fields.title.value }}
{{ content.fields.body.xml|raw }}
<figure>{{ ng_image(content.fields.image, 'large', {class: 'hero'}) }}</figure>
<img src="{{ ng_image_alias(content.fields.image, 'thumb').uri }}" width="…" height="…">
ng_image_alias returns an object with uri, width, height, name — use it when you need attributes the tag helper doesn’t expose.
Pagination. Wrap a Finder query directly with Pagerfanta using the shipped adapters (Netgen\EzPlatformSiteApi\Core\Site\Pagination\Pagerfanta\ContentSearchAdapter / LocationSearchAdapter), then render with pagerfanta(pager) in Twig. This avoids hand-rolling countContent + slice.
Integration tips.
Site/Finder via autowiring (aliases are registered) or with netgen.ezplatform_site.site / netgen.ezplatform_site.finder service IDs.$content->innerContent when you must call a legacy service that expects an eZ\Publish\API\Repository\Values\Content\Content.match entries like MyApp\Match\Foo: 123.ezplatform-site-api is frozen around eZ Platform 2.x (last release Feb 2023). On Ibexa you want netgen/ibexa-site-api (Netgen\IbexaSiteApi\*). Mixing the two is the most common source of "service not found" errors.withField('body', 'LIKE', '%foo%') scans content attribute rows and will crawl on large installs. For editorial full-text search use Solr with the native search API; keep the Finder for structured, location- or type-scoped listings.withIsVisible(true) (and withIsMainLocation(true) for content that has multiple locations) or you will leak drafts/invisible content into listings.$site->findContent() / the Finder are scoped to the current siteaccess root location. Content outside it returns null even though $repository->getContentService()->loadContent() would find it — a frequent surprise when debugging “missing” items from a CLI or admin context.content.fieldRelations or content.fields.related_items triggers additional repository calls. On a 100-item listing that’s 200 queries; either preload with withFieldRelation(), select only needed fields, or accept a bounded page size.ng_site_api.content_view, run bin/console cache:clear (and in prod, warm the cache). A miss falls back to the default ezplatform templates silently — no error, just the wrong output.ng_* Twig functions live in the bundle’s extension. “Unknown function ng_view” means the bundle isn’t registered for that kernel environment, or you are rendering from a bundle template that runs before the extension loads.ng_image/ng_image_alias will throw if the requested variation isn’t defined under system.<site>.image_variations in ezplatform.yml / ibexa.yaml. Define new variations there, not in ng_site_api.match. Id\Content: 42 (int) will not match '42' (string) from YAML in all cases — keep the value type consistent with what the matcher expects.netgen.ezplatform_site.site or ...finder, and register custom Query subclasses as services if you want a domain-specific DSL. The query builder is intentionally extensible; extending Query and adding whereX() helpers is the cleanest way to avoid duplicating criteria across controllers.is_visible / is_main_location conditions faster than stepping through the API.How can I help you explore Laravel packages today?