integrated/content-bundle
integrated/content-bundle is a Laravel package that bundles content management capabilities into a single integration, helping you organize, store, and serve structured content using familiar PHP and Laravel conventions.
Step 0 is access, not code. The repo lives on Bitbucket and is not on Packagist, so nothing works until your machine and CI can authenticate. Add a Composer VCS entry and provision a read-only Bitbucket app password (or SSH key) scoped to this repo:
"repositories": [
{ "type": "vcs", "url": "[email protected]:eactive/integrated-content-bundle.git" }
]
Then composer require integrated/content-bundle — but pin to a commit SHA or tag immediately, and prefer mirroring the repo into your own org (or a private Satis proxy) so builds never depend on eactive's availability or every dev's credentials.
Read these four things before anything else, in this order:
composer.json — require and autoload. This single file answers the only question that matters: is this a Symfony bundle (symfony/framework-bundle, symfony/twig-bundle, doctrine/orm deps, psr-4 under a Bundle namespace), a Laravel package (a ServiceProvider, illuminate/*), or framework-agnostic PHP? The -bundle suffix plus a Bitbucket host strongly suggests Symfony 2/3/4 era.*Bundle.php with getContainerExtension(), or a *ServiceProvider.php. That tells you the runtime it targets.DependencyInjection/ + Resources/config/ — the config tree and service wiring are the actual API surface. Everything you can configure is declared in the extension.Entity/ (or Model/) + Resources/views/ — the content domain model (types, blocks, pages) and the Twig templates, which is where the "render" half of the description lives.Your first use case — pick the honest one:
config/bundles.php, import its config, add the Doctrine mapping, run the migrations, create one simple content type (e.g. a "hero" block), and render it through its Twig template on a page. That is the end-to-end smoke test.ServiceProvider will register, no routes/views/config will be wired, and you'll be debugging a stack that isn't there. Instead, stand up a throwaway Symfony skeleton (composer create-project symfony/skeleton), wire the bundle there, and make one content type render. Your goal in that spike is not "make it run in Laravel" — it is to learn the domain model and decide whether to port the model or wrap the package. Budget one week and treat a failed boot as a valid, cheap answer.Only after that spike does a Laravel integration plan make sense — and it will almost certainly be "port the content model into Eloquent + Blade," not "install the bundle."
Wrap it. This is the whole strategy. Put your own contracts in front of the package from day one and never let its types reach a controller, Blade view, or queued job:
interface ContentRepository {
public function findBySlug(string $slug, string $locale): ?Content;
public function blocksFor(Content $content): array;
}
interface ContentRenderer {
public function render(Content $content): string;
}
The package becomes a replaceable adapter. This converts a private, undocumented dependency into a bounded swap if the vendor goes quiet — which, with zero dependents and no public tracker, you should assume will happen eventually.
Content-type-driven rendering. Expect the core workflow to be: an editor picks a block/content type in the admin surface → your code supplies the template for that type → the bundle composes the page. Day-to-day, a developer's job is defining that mapping and the templates, not editing stored content. Keep block templates in a predictable namespace with a fallback chain, so an unknown or new block type degrades to a neutral partial instead of a 500:
{% include [
'@Content/blocks/' ~ block.type ~ '.html.twig',
'@Content/blocks/_fallback.html.twig'
] with { block: block } only %}
Use only to keep block context isolated — it stops one block's variables leaking into another and makes blocks independently testable. If you port to Blade, mirror the same fallback logic with @includeFirst.
Cache rendered fragments, invalidate on write. Content rendering is read-heavy; do not re-walk the block tree per request. Render once and key the cache by content id + locale + a version/updated-at stamp. If the bundle owns its own cache layer (common in Symfony-era bundles), point it at your Redis rather than the filesystem — check its extension config for a cache pool/service setting before assuming.
Hook writes at the persistence boundary. In Symfony that's Doctrine lifecycle callbacks/event subscribers; in Laravel it's model events or an observer. Either way, invalidate the fragment cache there rather than in the admin controller — content also changes via imports, seeds, and console commands.
Localization. Assume content records are per-locale and you resolve locale in the repository, not the template. If the bundle has no locale concept, that's a schema change you own.
Admin surface. If the bundle ships its own admin controllers, decide early whether you adopt them or replace them with your app's resource CRUD (Filament/Nova/EasyAdmin). Adopting its admin means adopting its auth and routing assumptions; replacing it means the bundle is a renderer only — often the cleaner boundary.
Template override over forking. Symfony bundles support overriding templates by placing files under templates/bundles/<BundleName>/.... Prefer that, plus the config tree and tagged services, before you touch vendor code. Every line you fork is a line you re-merge.
Testing pattern that pays for itself: golden-file tests — render a fixture content tree and assert the exact output. That is your tripwire for a silent serialization or template change after an upgrade.
-bundle in a Laravel app is not an install, it's a port. Its DI config, config tree, Doctrine mappings, and Twig templates have no runtime equivalent in Laravel's container/Eloquent/Blade stack. Confirm this in composer.json before you spend an hour on composer require.bin/console debug:container --tag=<tag>, debug:config <alias>, and debug:router. There is no Stack Overflow corpus for this package — the source is the documentation.|raw in Twig or {!! !!} in Blade around content stored by editors is the classic mistake. Sanitize on save (HTMLPurifier or equivalent) and escape by default in every template.{{ block }}. Render leaf fields explicitly.composer.lock, and keep Composer credentials read-only and out of CI logs — rotating an expired Bitbucket app password should not be able to break a production build.How can I help you explore Laravel packages today?