Skip to content

Instantly share code, notes, and snippets.

Last active Feb 6, 2019
What would you like to do?
StringReferenceProcessor to resolve id references to others fixtures when not set in a relation
namespace App\Bundle\DependencyInjection;
use App\Fixtures\StringReferenceProcessor;
use Sonata\ArticleBundle\Model\FragmentInterface;
use Sonata\BlockBundle\Model\BlockInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
* Class AppExtension.
* @author Romain Mouillard <>
class AppExtension extends Extension
public function load(array $configs, ContainerBuilder $container): void
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$fixturesConfig = [
FragmentInterface::class => ['fields'],
BlockInterface::class => ['settings'],
$stringReference = $container->getDefinition(StringReferenceProcessor::class);
$stringReference->setArgument('$config', $fixturesConfig);
block_test_{a, b, c}:
page: '@page_test'
parent: '@block_contaier_test'
name: Block Test <current()>
type: block.type
enabled: true
position: 1
media: '#media_test->id'
- { media: '#media_test_<current()>->id' }
<?xml version="1.0" ?>
<container xmlns=""
<defaults autowire="true" autoconfigure="true" public="false" />
<service id="App\Fixtures\StringReferenceProcessor" class="App\Fixtures\StringReferenceProcessor">
<tag name="fidry_alice_data_fixtures.processor" />
<call method="setLogger">
<argument type="service" id="logger" on-invalid="ignore"/>
namespace App\Fixtures;
use Doctrine\ORM\EntityManagerInterface;
use Fidry\AliceDataFixtures\ProcessorInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
* Class StringReferenceProcessor.
* Alice fixtures 3.x does not resolve anymore the id references to others fixtures when not set in a relation.
* This is an issue with array fields like sonata block settings or sonata fragment fields which are arrays and
* might contain such references to other fixtures (a media id for example).
* This processor resolves this new pattern in the fixtures yaml:
* #fixture_name->property
* Resolve is done after all entities are persisted a first time, so ids are available to replace the pattern.
* @author Romain Mouillard <>
final class StringReferenceProcessor implements ProcessorInterface
use LoggerAwareTrait;
* @var EntityManagerInterface
private $entityManager;
* @var array
private $fixtures = [];
* @var PropertyAccessor
private $propertyAccessor;
* Fields to resolve in classes.
* Keys are classes or interfaces, values an array of fields to search in to resolve.
* @var array
private $config;
public function __construct(EntityManagerInterface $entityManager, array $config = [])
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
$this->logger = new NullLogger();
$this->entityManager = $entityManager;
$this->config = $config;
public function preProcess(string $id, $object): void
// Keep track of all fixtures id and associated object
$this->fixtures[$id] = $object;
public function postProcess(string $id, $object): void
// Retrieve configs to apply on this object
$configs = array_filter(
function ($key) use ($object) {
// We want config which applies to the object class
return $object instanceof $key;
// Return early if this object is not handled by this processor
if (count($configs) === 0) {
foreach ($configs as $config => $fields) {
$this->resolve($fields, $id, $object);
* Resolve a set fields of the given object
* This method is recursively analysing the fields on the provided object
* and replaces any instances of "#fixture_name->property" by the actual value.
* @param array $fields an array of fields to resolve
* @param string $id the identifier of the object in the fixtures
* @param object $object the object that is the subject of resolve
private function resolve(array $fields, string $id, $object): void
$needsPersist = false;
foreach ($fields as $field) {
$isFieldUpdated = false;
$settingsArray = $this->propertyAccessor->getValue($object, $field);
// No need to carry on if no array in field
if (!$settingsArray || !is_array($settingsArray)) {
// Walk into fields to find some "#fixture_name->property" pattern
// Field value is passed as reference, any change to it will be applied in the settings
// NeedsPersist value is passed as reference since it can be updated within the callable
array_walk_recursive($settingsArray, function (&$value) use ($id, &$isFieldUpdated): void {
$matches = [];
if (preg_match('/\#([a-zA-Z0-9_]*)->(.*)/', (string) $value, $matches)) {
$this->logger->debug(sprintf('Found pattern to replace in fixture "%s": %s', $id, $value));
$fixtureName = $matches[1];
$fixtureProperty = $matches[2];
if (!isset($this->fixtures[$fixtureName])) {
throw new \UnexpectedValueException(sprintf('Could not find fixture with name "%s"', $fixtureName));
$fixtureObject = $this->fixtures[$fixtureName];
$resolvedValue = $this->propertyAccessor->getValue($fixtureObject, $fixtureProperty);
$this->logger->debug(sprintf('Resolved pattern "%s" to "%s"', $value, $resolvedValue));
$value = $resolvedValue;
$isFieldUpdated = true;
// Update field in the object if modified
if ($isFieldUpdated) {
$this->propertyAccessor->setValue($object, $field, $settingsArray);
$needsPersist = true;
// Persists changes on the entity
if ($needsPersist) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment