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.php
New structure:
src/StateProvider/Shop/Product/CollectionProvider.php
src/StateProvider/Admin/Product/CollectionProvider.php
src/StateProvider/Shop/Product/ItemProvider.php
Pattern: StateProvider/{Section}/{Resource}/{Type}Provider.php
Section:
Shop
orAdmin
Resource: Entity name (singular)
Type:
CollectionProvider
orItemProvider
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\Banner
Class name:
GetAdsBannersDataProvider
→CollectionProvider
Interface:
ContextAwareCollectionDataProviderInterface
→ProviderInterface
Method:
getCollection()
→provide()
Removed:
supports()
method - no longer neededRemoved:
$class
parameter - no longer neededAdded:
Operation $operation
parameterClass modifiers: Added
final readonly
PHPDoc: 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:
$class
argument (resource class parameter)Tag:
api_platform.collection_data_provider
→api_platform.state_provider
Priority: 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.xml
7. Validate Changes
Clear the cache:
vendor/bin/console cache:clear
Verify routes are registered:
vendor/bin/console debug:router | grep your_plugin_api
Verify StateProvider is registered:
vendor/bin/console debug:container | grep state_provider
Test 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
provider
attributeRemove $class parameter: Resource class is obtained from
$operation->getClass()
readonly classes: Follow Sylius convention with
final readonly class
Return types:
Collections:
array
Items:
object|null
Processors:
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?