9.3 API: Migrate DataProvider to StateProvider
This step migrates API Platform 2.x DataProviders and DataPersisters to API Platform 4.x StateProviders and StateProcessors.
When to skip this step:
Your plugin doesn't have custom DataProvider or DataPersister classes
When to do this step:
You have custom DataProvider classes in
src/DataProvider/You have custom DataPersister classes in
src/DataPersister/
Overview of Changes
API Platform 4.x replaced DataProviders and DataPersisters with a unified State system:
CollectionDataProviderInterface
ProviderInterface (collections)
ItemDataProviderInterface
ProviderInterface (items)
DataPersisterInterface
ProcessorInterface
DataTransformerInterface
Some → SerializerContextBuilder
Key changes:
New interfaces and namespaces
Different method signatures
Explicit linking to operations (no more
supports()method)Organized directory structure
1. Identify Files to Migrate
Check if you have DataProviders:
find src/DataProvider -name "*.php"Check if you have DataPersisters:
find src/DataPersister -name "*.php"2. Migrate DataProvider to StateProvider
Directory Structure
Follow Sylius conventions for organization:
Old structure:
src/DataProvider/GetProductsDataProvider.phpNew structure:
src/StateProvider/Shop/Product/CollectionProvider.php
src/StateProvider/Admin/Product/CollectionProvider.php
src/StateProvider/Shop/Product/ItemProvider.phpPattern: StateProvider/{Section}/{Resource}/{Type}Provider.php
Section:
ShoporAdminResource: Entity name (singular)
Type:
CollectionProviderorItemProvider
Example Migration
Before (API Platform 2.x):
src/DataProvider/GetAdsBannersDataProvider.php:
<?php
declare(strict_types=1);
namespace BitBag\SyliusBannerPlugin\DataProvider;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use BitBag\SyliusBannerPlugin\Repository\AdRepositoryInterface;
final class GetAdsBannersDataProvider implements
ContextAwareCollectionDataProviderInterface,
RestrictedDataProviderInterface
{
public function __construct(
private AdRepositoryInterface $adRepository,
private BannersProviderInterface $bannersProvider,
private string $class,
) {
}
public function supports(
string $resourceClass,
string $operationName = null,
array $context = [],
): bool {
return $this->class === $resourceClass;
}
public function getCollection(
string $resourceClass,
string $operationName = null,
array $context = [],
): iterable {
$localeCode = $context['filters']['locale_code'] ?? null;
$sectionCode = $context['filters']['section_code'] ?? null;
if (null !== $localeCode && null !== $sectionCode) {
$ads = $this->adRepository->findAllActiveAds();
return $this->bannersProvider->getAdsBanners($ads, $sectionCode, $localeCode);
}
return [];
}
}After (API Platform 4.x):
src/StateProvider/Shop/Banner/CollectionProvider.php:
<?php
declare(strict_types=1);
namespace BitBag\SyliusBannerPlugin\StateProvider\Shop\Banner;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use BitBag\SyliusBannerPlugin\Entity\BannerInterface;
use BitBag\SyliusBannerPlugin\Provider\BannersProviderInterface;
use BitBag\SyliusBannerPlugin\Repository\AdRepositoryInterface;
/** @implements ProviderInterface<BannerInterface> */
final readonly class CollectionProvider implements ProviderInterface
{
public function __construct(
private AdRepositoryInterface $adRepository,
private BannersProviderInterface $bannersProvider,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
$localeCode = $context['filters']['locale_code'] ?? null;
$sectionCode = $context['filters']['section_code'] ?? null;
if (null !== $localeCode && null !== $sectionCode) {
$ads = $this->adRepository->findAllActiveAds();
return $this->bannersProvider->getAdsBanners($ads, $sectionCode, $localeCode);
}
return [];
}
}Key Changes:
Namespace:
DataProvider→StateProvider\Shop\BannerClass name:
GetAdsBannersDataProvider→CollectionProviderInterface:
ContextAwareCollectionDataProviderInterface→ProviderInterfaceMethod:
getCollection()→provide()Removed:
supports()method - no longer neededRemoved:
$classparameter - no longer neededAdded:
Operation $operationparameterClass modifiers: Added
final readonlyPHPDoc: Added
@implements ProviderInterface<BannerInterface>
3. Update Service Registration
Before:
config/services/dataProvider/dataProvider.xml:
<service id="bitbag.sylius_banner_plugin.data_provider.get_ads_banners_data_provider"
class="BitBag\SyliusBannerPlugin\DataProvider\GetAdsBannersDataProvider"
>
<argument type="service" id="bitbag_sylius_banner_plugin.repository.ad"/>
<argument type="service" id="bitbag.sylius_banner_plugin.provider.banners_provider"/>
<argument>%bitbag_sylius_banner_plugin.model.banner.class%</argument>
<tag name="api_platform.collection_data_provider"/>
</service>After:
config/services/stateProvider/stateProvider.xml:
<service id="bitbag.sylius_banner_plugin.state_provider.shop.banner.collection_provider"
class="BitBag\SyliusBannerPlugin\StateProvider\Shop\Banner\CollectionProvider"
>
<argument type="service" id="bitbag_sylius_banner_plugin.repository.ad"/>
<argument type="service" id="bitbag.sylius_banner_plugin.provider.banners_provider"/>
<tag name="api_platform.state_provider" priority="10"/>
</service>Changes:
Service ID: Follow pattern
{vendor}.{plugin}.state_provider.{section}.{resource}.{type}Class: Updated to new namespace
Removed:
$classargument (resource class parameter)Tag:
api_platform.collection_data_provider→api_platform.state_providerPriority: Added
priority="10"
4. Link StateProvider to Operations
In API Platform 4.x, you must explicitly link providers to operations.
config/api_resources/resources/shop/Banner.xml:
Before (implicit):
<operation name="get_banners" class="ApiPlatform\Metadata\GetCollection" uriTemplate="/shop/banners">
<!-- DataProvider was matched via supports() method -->
</operation>After (explicit):
<operation
name="get_banners"
class="ApiPlatform\Metadata\GetCollection"
uriTemplate="/shop/banners"
provider="bitbag.sylius_banner_plugin.state_provider.shop.banner.collection_provider"
>
<!-- StateProvider explicitly linked via provider attribute -->
</operation>Add the provider="{service_id}" attribute to link your StateProvider to the operation.
5. Migrate DataPersister to StateProcessor (if applicable)
If you have DataPersisters, migrate them similarly:
Before:
final class CreateProductDataPersister implements DataPersisterInterface
{
public function persist($data, array $context = [])
{
$this->entityManager->persist($data);
$this->entityManager->flush();
return $data;
}
public function remove($data, array $context = [])
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
}After:
/** @implements ProcessorInterface<ProductInterface> */
final readonly class CreateProcessor implements ProcessorInterface
{
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
$this->entityManager->persist($data);
$this->entityManager->flush();
return $data;
}
}Service registration:
<service id="vendor.plugin.state_processor.shop.product.create"
class="Vendor\Plugin\StateProcessor\Shop\Product\CreateProcessor"
>
<tag name="api_platform.state_processor" priority="10"/>
</service>Link to operation:
<operation
name="create_product"
class="ApiPlatform\Metadata\Post"
processor="vendor.plugin.state_processor.shop.product.create"
>
</operation>6. Remove Old Files
After migration is complete and tested:
# Remove old directories
rm -rf src/DataProvider
rm -rf src/DataPersister
# Remove old service configurations
rm config/services/dataProvider/dataProvider.xml
rm config/services/dataPersister/dataPersister.xml7. Validate Changes
Clear the cache:
vendor/bin/console cache:clearVerify routes are registered:
vendor/bin/console debug:router | grep your_plugin_apiVerify StateProvider is registered:
vendor/bin/console debug:container | grep state_providerTest API endpoints:
curl -X GET http://localhost/api/v2/shop/banners -H "Accept: application/json"Common Patterns
Pattern: Item Provider
For getting a single item:
/** @implements ProviderInterface<ProductInterface> */
final readonly class ItemProvider implements ProviderInterface
{
public function __construct(
private ProductRepositoryInterface $productRepository,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
{
return $this->productRepository->find($uriVariables['id']);
}
}Pattern: Using URI Variables
Access path parameters via $uriVariables:
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
{
$productCode = $uriVariables['code'];
return $this->productRepository->findOneByCode($productCode);
}Pattern: Using Filters
Access query parameters via $context['filters']:
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
$category = $context['filters']['category'] ?? null;
$enabled = $context['filters']['enabled'] ?? true;
return $this->productRepository->findBy([
'category' => $category,
'enabled' => $enabled,
]);
}Important Notes
No supports() method: Linking is now explicit via
providerattributeRemove $class parameter: Resource class is obtained from
$operation->getClass()readonly classes: Follow Sylius convention with
final readonly classReturn types:
Collections:
arrayItems:
object|nullProcessors:
mixed
Service ID format:
{vendor}.{plugin}.state_provider.{section}.{resource}.{type}Priority: Use
priority="10"in service tags
Reference
For more examples, check Sylius core StateProviders:
vendor/sylius/sylius/src/Sylius/Bundle/ApiBundle/StateProvider/Last updated
Was this helpful?
