Skip to content

Instantly share code, notes, and snippets.

@Wharenn
Last active Feb 6, 2019
Embed
What would you like to do?
StringReferenceProcessor to resolve id references to others fixtures when not set in a relation
<?php
declare(strict_types=1);
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 <romain.mouillard@ekino.com>
*/
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'));
$loader->load('services.xml');
$fixturesConfig = [
FragmentInterface::class => ['fields'],
BlockInterface::class => ['settings'],
];
$stringReference = $container->getDefinition(StringReferenceProcessor::class);
$stringReference->setArgument('$config', $fixturesConfig);
}
}
App\Entity\Block:
block_test_{a, b, c}:
page: '@page_test'
parent: '@block_contaier_test'
name: Block Test <current()>
type: block.type
enabled: true
position: 1
settings:
brand:
media: '#media_test->id'
url: http://www.github.com/
menu:
elements:
- { media: '#media_test_<current()>->id' }
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<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"/>
</call>
</service>
</services>
</container>
<?php
declare(strict_types=1);
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 <romain.mouillard@ekino.com>
*/
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(
$this->config,
function ($key) use ($object) {
// We want config which applies to the object class
return $object instanceof $key;
},
ARRAY_FILTER_USE_KEY
);
// Return early if this object is not handled by this processor
if (count($configs) === 0) {
return;
}
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)) {
continue;
}
// 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) {
$this->entityManager->persist($object);
$this->entityManager->flush();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment