open-dxp/opendxp
OpenDXP is an open-source digital experience platform built on PHP and Symfony. It provides flexible tools and APIs for managing content, product data, digital assets, and customer experiences, making it easier to build and integrate modern web applications.
First, reset your expectations: open-dxp/opendxp is not a Laravel package. It is a Symfony 6 application (a community fork of Pimcore, rebranded) that ships as a full platform — admin UI, Doctrine, Twig, its own console. There is no service provider to register, no config/opendxp.php to publish, no Eloquent models, and no artisan command. You cannot composer require it into an existing Laravel app and mount it on a route. The realistic architecture for a Laravel developer is two applications: OpenDXP as the PIM/DAM/headless content backend, Laravel as the transactional app, talking over HTTP.
Minimal steps:
composer create-project open-dxp/skeleton opendxp , fill in DB/Redis credentials in .env, run the installer console command, then point a vhost at public/. Use bin/console — not artisan.config/ (Symfony bundles + platform config), src/ (your custom controllers/listeners — this is where your code lives), templates/ (Twig, areabricks), and var/classes/ (generated DataObject PHP classes — read-only).Product class in the admin UI with 3–4 fields, generate the PHP class, expose it through a GraphQL query (DataHub bundle) or a 20-line Symfony controller, then consume it from Laravel with Http::post(). That single loop teaches you the whole mental model.Your first real use case is almost always: product/content data authored in OpenDXP, rendered by your Laravel storefront or API.
Headless API contract (the main pattern). Treat OpenDXP as a content API. Either enable the DataHub bundle and define a GraphQL schema in the admin UI, or write a thin Symfony controller under src/Controller/Api/ that returns JSON. In Laravel, wrap it in one typed client class per domain and bind it in a service provider — never call Http:: from controllers:
// app/Services/OpenDxp/ProductCatalog.php
class ProductCatalog
{
public function __construct(
private readonly string $endpoint,
private readonly string $apiKey,
) {}
public function product(string $sku): ?array
{
return Cache::remember("opendxp.product.$sku", 600, function () use ($sku) {
$res = Http::withHeaders(['X-API-Key' => $this->apiKey])
->timeout(5)->retry(2, 200)
->post($this->endpoint, [
'query' => 'query($sku:String!){ getProduct(sku:$sku){ id sku name price{ amount currency } } }',
'variables' => ['sku' => $sku],
]);
return $res->successful() ? data_get($res->json(), 'data.getProduct') : null;
});
}
}
Outbound events instead of polling. For "PIM record changed → update Laravel search index / price cache", register a Symfony event listener inside OpenDXP that POSTs a small payload to a Laravel webhook endpoint. Namespace prefixes follow the fork (OpenDxp\... vs upstream Pimcore\...) — verify against vendor/open-dxp/ before copying any Pimcore tutorial verbatim. Push those webhooks onto a Laravel queue and make the receiver idempotent.
DAM as the asset origin. Store images/videos in OpenDXP, let it generate thumbnails, and have Laravel persist only the asset ID + a derived thumbnail URL. Don't duplicate binaries into Laravel's storage; do put a CDN in front of OpenDXP's asset route.
Local dev topology. Three containers: opendxp (PHP-FPM + nginx), mysql, redis. Laravel keeps its own DB — do not point both apps at the same MySQL instance and start joining tables; the schema is Doctrine-generated and unstable across releases. Separate hosts in .env: OPENDXP_URL=http://opendxp.test, and enable CORS only if a browser talks to it directly (it usually shouldn't).
Auth split. Keep the OpenDXP admin as a back-office tool with its own user table. Laravel's users are customers. If you need shared identity, do it with an OIDC provider in front of both rather than trying to sync password hashes.
Rendering split. If a marketing page is fully editorial, you can serve it from OpenDXP's Twig + document types (pages/snippets, areabricks for block editing). Most Laravel teams find it cleaner to keep one renderer and let Laravel's Blade/Twig-ish layer consume content JSON, so editors get areabricks and developers stay in one codebase.
var/ is generated code. The PHP classes in var/classes/ are rebuilt from the class definition stored in the DB. Never hand-edit them — changes vanish on cache clear or class regeneration. Put behaviour in a custom parent class that the generated class extends; that survives regeneration.bin/console cache:clear / the platform's cache-clear command, plus a hard browser refresh for the admin UI's JS. "It didn't work" is a cache symptom 80% of the time.intl, gd, exif, pdo_mysql, high memory_limit (256M+ minimum, 512M realistic), and a specific PHP minor version. Install failures are usually a missing extension, not a Composer problem.null in one locale and populated in another for reasons not visible in your query. Always request _language-scoped data explicitly and document fallback behaviour on the Laravel side.NOASSERTION — verify before shipping. Upstream Pimcore is GPLv3; a fork very likely inherits copyleft. That is a materially different obligation from Laravel's MIT. Get a legal read before bundling it into a distributed product, and definitely before linking it into proprietary code.pimcore:/opendxp: console prefixes. Pin your composer version and read every changelog; a young fork's upgrade path is not a solved problem.config/ (editorial approval gates — note each workflow action needs a service you implement), and DataHub schema exports (commit them to git so API shape changes are reviewable).src/Controller/Api/ with explicit pagination will outperform one flexible schema in day-to-day use.How can I help you explore Laravel packages today?