PropertyResolver and ResourceLoader
====================================
PropertyResolvers and ResourceLoaders are core services in Sulu's content resolution system
that work together to efficiently transform and load content data for the website frontend.
They provide a clean separation of concerns: PropertyResolvers define **what** data should
be loaded and **how** it should be transformed, while ResourceLoaders handle the actual
**batch loading** of resources from the database or external sources.
This architecture enables performance optimizations through batch loading and provides
clear extension points for custom property types. You'll need to create custom
PropertyResolvers and ResourceLoaders when building selection fields for custom entities
or integrating external data sources into Sulu's content system.
The Content Resolution Process
-------------------------------
Understanding how PropertyResolvers and ResourceLoaders collaborate helps you implement
them correctly. The content resolution process follows these steps:
1. **Content Resolution Start**: The ``ContentResolver`` receives a request to resolve
content for a page or content entity.
2. **Property Resolution**: For each property in the template, the appropriate
``PropertyResolver`` is called based on the property type. The resolver transforms
the raw data (typically IDs) into a ``ContentView`` object containing
``ResolvableResource`` placeholders.
3. **Priority Queue**: All ``ResolvableResource`` objects are collected in a priority
queue. Resources with the same priority and loader key are grouped together for
batch loading.
4. **Batch Loading**: The ``ResolvableResourceLoader`` processes the queue by priority,
calling the appropriate ``ResourceLoader`` for each group. ResourceLoaders fetch
multiple resources in a single database query.
5. **Resource Replacement**: The ``ResolvableResourceReplacer`` replaces all
``ResolvableResource`` placeholders with the actual loaded data.
6. **Nested Resolution**: If loaded resources are ContentRichEntities (like pages or
custom entities), they are recursively resolved, repeating the process at the next
depth level.
7. **Final Output**: The fully resolved content is returned, ready for rendering in Twig
templates.
This batch loading approach prevents N+1 query problems and ensures optimal performance
even with complex nested content structures.
How to create a custom PropertyResolver?
-----------------------------------------
A PropertyResolver transforms raw property data into a structured ``ContentView`` object.
You need to create a custom PropertyResolver when implementing selection fields for
custom entities or when building property types that reference external data.
PropertyResolvers must implement the ``PropertyResolverInterface`` which requires two
methods:
* ``resolve(mixed $data, string $locale, array $params = []): ContentView`` - Transforms
the raw data into a ContentView
* ``getType(): string`` - Returns the property type identifier
1. PropertyResolver Implementation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here's a complete example of a PropertyResolver for selecting products:
.. code-block:: php
[], ...$params]);
}
// Extract product IDs from data
$ids = $data;
// Get ResourceLoader key (allow override via params)
$resourceLoaderKey = $params['resourceLoader'] ?? ProductResourceLoader::getKey();
// Create ContentView with resolvable resources
return ContentView::createResolvablesWithReferences(
ids: $ids,
resourceLoaderKey: $resourceLoaderKey,
resourceKey: Product::RESOURCE_KEY,
view: [
'ids' => $ids,
...$params,
],
priority: 150,
metadata: [
'properties' => $params['properties'] ?? null,
]
);
}
public static function getType(): string
{
return 'product_selection';
}
}
**Key Points:**
* **Data Validation**: Always validate input data and return an empty ContentView for
invalid data
* **ContentView Factory Methods**: Use ``createResolvablesWithReferences()`` for multiple
resources that should create reference entries. Use ``createResolvable()`` for single
resources or ``create()`` for simple data that doesn't need loading
* **Priority Values**: Convention is ``-50`` for links and media, ``0`` for default/simple
types, ``100`` for content entities like articles or snippets, and ``150`` for pages.
Higher values are reserved for special cases (e.g., ``2048`` for ``SmartResolvable``).
Resources with the same priority and loader key are batched together
* **Metadata**: Pass metadata to control which properties are resolved for nested content
entities
* **Resource Key**: The resource key (e.g., ``Product::RESOURCE_KEY``) is used for
reference tracking and cache invalidation
2. Service Definition
~~~~~~~~~~~~~~~~~~~~~
Register the PropertyResolver as a service with the ``sulu_content.property_resolver`` tag:
.. code-block:: yaml
# config/services.yaml
services:
App\Content\PropertyResolver\ProductSelectionPropertyResolver:
tags:
- { name: 'sulu_content.property_resolver' }
.. note::
With autowiring enabled (the default in Sulu), you don't need to manually register
these services. Sulu will automatically apply the ``sulu_content.property_resolver``
tag to all services implementing ``PropertyResolverInterface`` and the
``sulu_content.resource_loader`` tag to all services implementing
``ResourceLoaderInterface``.
The service tag uses the ``getType()`` method to automatically index the resolver by its
type. When a property with ``type="product_selection"`` is encountered, Sulu will use
this resolver.
3. Template Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the custom property type in your page templates:
.. code-block:: xml
Product ID: {{ product.id }}
No products selected.
{% endif %} Best Practices -------------- **PropertyResolver:** * Always validate input data and return an empty ContentView for invalid cases (never throw exceptions) * Choose the appropriate ContentView factory method: * ``createResolvablesWithReferences()`` - Entity selections (enables cache invalidation) * ``createResolvable()`` - Single resource reference * ``create()`` - Simple data without loading * Use standard priority values: ``-50`` (links/media), ``0`` (default), ``100`` (articles/snippets), ``150`` (pages) **ResourceLoader:** * **Always implement batch loading** - load all resources in a single query, never one by one * Return results indexed by ID as required by the interface * Handle missing resources gracefully by omitting them from results * Implement security/permission checks here, not in PropertyResolvers * Respect locale parameters and allow filter parameters for advanced scenarios **Common Pitfalls:** * ❌ Don't load resources in PropertyResolvers - that's the ResourceLoader's exclusive job * ❌ Don't forget to index ResourceLoader results by ID .. note:: PropertyResolvers and ResourceLoaders are designed for read operations on the website frontend. For admin interfaces and write operations, use Sulu's Admin API and form metadata system instead.