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

Contentful Bundle Laravel Package

markup/contentful-bundle

Symfony bundle that integrates the Contentful content delivery API into your application, providing configurable client services and convenient access to content entries and assets from Contentful spaces.

View on GitHub
Deep Wiki
Context7

Getting Started

This bundle wraps the Contentful Content Delivery API PHP SDK as a Symfony service. It is a 2016-era Symfony 2/3 bundle — the last release was October 2016, it has 2 stars and no dependents. Treat the setup below as a "make it work, then isolate it" exercise.

1. Install (with a pinned, explicit constraint):

composer require markup/contentful-bundle:"^1.0" --ignore-platform-reqs

The --ignore-platform-reqs flag is often needed on modern PHP because the bundle's composer.json will constrain PHP to 5.x/7.0-era versions. If Composer refuses outright, install from the VCS repository instead:

"repositories": [{ "type": "vcs", "url": "https://github.com/usemarkup/ContentfulBundle" }]

2. Register the bundle. On Symfony 2/3, add it to AppKernel::registerBundles(). On Symfony 4+, Flex will not auto-discover it — add it manually to config/bundles.php and expect to patch service definitions (see Gotchas).

3. Read the config contract before writing config. Do not guess the config keys. Open these two files in vendor/markup/contentful-bundle/:

  • DependencyInjection/Configuration.php — the authoritative tree of config keys.
  • Resources/config/services.xml (or .yml) — the authoritative service IDs and constructor arguments.

A configuration in the shape you should expect looks like:

# app/config/config.yml
markup_contentful:
    space_id:     "%contentful_space_id%"
    access_token: "%contentful_access_token%"
    environment:  master        # verify this key exists in Configuration.php

4. First use case — fetch and render a list of entries. Get the client service by the ID you found in services.xml and query a content type:

$client  = $this->get('markup_contentful.client'); // confirm the real ID
$entries = $client->getEntries([
    'content_type' => 'blogPost',
    'limit'        => 10,
    'order'        => '-sys.createdAt',
]);

return $this->render('blog/index.html.twig', ['entries' => $entries]);

Once that renders, you have validated: credentials, space, content model, and the SDK version the bundle pins. Do that before building anything else.

Implementation Patterns

Wrap the client immediately. Do not inject the bundle's service into controllers, Twig extensions, or commands. Create one interface and one implementation:

interface ArticleRepository
{
    /** @return Article[] */
    public function latest(int $limit = 10): array;
}

final class ContentfulArticleRepository implements ArticleRepository
{
    public function __construct(private object $client, private CacheItemPoolInterface $cache) {}

    public function latest(int $limit = 10): array
    {
        // query, map to DTOs, cache
    }
}

Configure this as an alias for the interface. Every future migration (new SDK, direct HTTP calls, another CMS) becomes a one-line service swap rather than a grep across the codebase. Given this bundle's maintenance status, this is not optional hygiene — it is the whole point.

Map SDK objects to your own DTOs at the boundary. The Contentful SDK returns Entry objects with a getFields() array and a sys metadata object. Consumers of your repository should never touch sys, Link, or ArrayAccess. Hydrate DTOs explicitly so field-name changes and locale changes are contained in one class.

Cache at the query level, keyed by the query. Contentful calls are network-bound and rate-limited. Use a PSR-6 pool with a key derived from the normalized query plus locale:

$key = 'cf.' . md5(json_encode([$contentType, $limit, $locale, $include]));

Set a TTL (5–15 minutes is typical) and add a stale-while-revalidate style fallback: on a Contentful exception, serve the stale item rather than a 500.

Invalidate via webhooks, not TTL alone. Configure a Contentful webhook to hit a Symfony route. Contentful posts the entry ID and content type; your endpoint clears cache tags or the specific keys. Tag your cache items with cf.entry.{id} and cf.type.{contentType} so invalidation is surgical rather than a full pool flush.

Prefetch in the controller, not lazily in Twig. A Twig function that calls Contentful inside a loop will issue one request per iteration. Build all the data the template needs in the controller (or a view model), pass arrays in, and keep Twig dumb.

Separate preview from delivery. If the bundle exposes a preview client or preview flag (check Configuration.php), instantiate a second client for editorial previews using the preview access token. Otherwise write your own factory and register it as a second service. Never reuse the delivery client for preview — different host, different token, different caching semantics.

Testing: never hit Contentful in unit tests. Because you wrapped the client, mock your repository interface in controllers, and unit-test the repository against fixtures — either hand-built Entry objects or recorded JSON fixtures decoded into arrays and fed through a small factory. This also protects you when the bundle eventually stops installing.

Migrations and content modeling: keep field name constants in the repository implementation (const FIELD_SLUG = 'slug';). Contentful field IDs are case-sensitive and cannot be renamed without a migration; centralizing them turns a silent null into a compile-time-ish error.

Gotchas and Tips

It is effectively abandoned. Last release October 2016, 2 stars, zero dependents. There is no security-patch path, no Symfony 4/5/6 support, and no maintainer. Use it for a legacy application already on it, or as a reference implementation for wiring the Contentful SDK into Symfony's DI. For greenfield work, register the Contentful SDK directly as a service — it is the same amount of code and you control the lifecycle.

Symfony 4+ service visibility will bite you. The bundle's services.xml predates private-by-default services and typically declares services public="true". On Symfony 4+, $this->get() in a controller will fail (or trigger deprecations) — inject the service, and if a definition is private, add public: true via a compiler pass or override the definition in config/services.yaml.

PHP 8 incompatibilities are likely. 2016-era code frequently uses each(), implicit nullable parameters, or \ReflectionParameter::getClass(). Expect deprecation noise at minimum and fatal errors at worst. Run php -l across vendor/markup/contentful-bundle/ after install, and prefer PHP 7.4 if you have the choice.

Do not upgrade the Contentful SDK underneath the bundle. The bundle pins an SDK major (roughly the 1.x/2.x PHP SDK era). That SDK's API — getEntries, getEntry, Query, Link resolution via include — differs materially from the modern contentful/contentful.php (namespaces and client construction changed). Upgrading the SDK without rewriting the bundle's service definitions and your call sites will break at runtime, not at composer install.

Includes are depth-limited. By default the SDK resolves linked entries only a couple of levels deep. If your template shows author.name on a post, set 'include' => 2 (or higher) explicitly. When the limit is exceeded you get an unresolved Link object, and Twig will silently render nothing rather than throw.

Rate limits and 429s. Contentful's delivery API tolerates a healthy burst, but a CLI import, a sitemap generator, or a paginated crawl will hit RateLimitExceededException. Wrap calls in a retry with exponential backoff (3 attempts, jitter) at the repository layer — not scattered through call sites.

Do not serialize SDK objects. Entry objects hold references that do not survive serialize()/unserialize() cleanly across SDK versions. Cache plain arrays (or your DTOs) and rebuild SDK objects on read.

Locale is a query concern. Pass locale on every query; do not rely on a client-level default unless Configuration.php exposes one. A missing locale silently falls back to the space default, which is a subtle bug in multi-locale setups.

Extension points. Two clean ways to bend the bundle without forking: (1) decorate the client service (decorates: in services.yaml) to add logging, retries, and metrics around every call; (2) replace the definition entirely with your own factory that constructs the SDK client from %env()% parameters. Either keeps the bundle as a thin bootstrap while you own the actual behavior — and either makes removing it later a non-event.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core