Skip to content

Commit

Permalink
Improve fluent resolvers when using aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
mcg-web committed Feb 11, 2018
1 parent 4720e0c commit 10b1660
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 117 deletions.
36 changes: 16 additions & 20 deletions Command/DebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,38 +74,34 @@ protected function execute(InputInterface $input, OutputInterface $output)

/** @var FluentResolverInterface $resolver */
$resolver = $this->{$category.'Resolver'};

$solutions = $this->retrieveSolutions($resolver);
$this->renderTable($tableHeaders, $solutions, $io);
$this->renderTable($resolver, $tableHeaders, $io);
}
}

private function renderTable(array $tableHeaders, array $solutions, SymfonyStyle $io)
private function renderTable(FluentResolverInterface $resolver, array $tableHeaders, SymfonyStyle $io)
{
$tableRows = [];
foreach ($solutions as $id => &$options) {
ksort($options['aliases']);
$tableRows[] = [$id, implode("\n", $options['aliases'])];
$solutionIDs = array_keys($resolver->getSolutions());
sort($solutionIDs);
foreach ($solutionIDs as $solutionID) {
$aliases = $resolver->getSolutionAliases($solutionID);
$aliases[] = $solutionID;
$options = $resolver->getSolutionOptions($solutionID);
$tableRows[$options['id']] = [$options['id'], self::serializeAliases($aliases, $options)];
}
ksort($tableRows);
$io->table($tableHeaders, $tableRows);
$io->write("\n\n");
}

private function retrieveSolutions(FluentResolverInterface $resolver)
private static function serializeAliases(array $aliases, array $options)
{
$data = [];
foreach ($resolver->getSolutions() as $alias => $solution) {
$options = $resolver->getSolutionOptions($alias);

$id = $options['id'];
if (!isset($data[$id]['aliases'])) {
$data[$id]['aliases'] = [];
}
$data[$id]['aliases'][] = $options['alias'].(isset($options['method']) ? ' (method: '.$options['method'].')' : '');
}
ksort($data);
ksort($aliases);
$aliases = array_map(function ($alias) use ($options) {
return $alias.(isset($options['method']) ? ' (method: '.$options['method'].')' : '');
}, $aliases);

return $data;
return implode("\n", $aliases);
}

public static function getCategories()
Expand Down
5 changes: 3 additions & 2 deletions Config/Processor/NamedConfigProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ final class NamedConfigProcessor implements ProcessorInterface
public static function process(array $configs)
{
foreach ($configs as $name => &$config) {
$config['config'] = isset($config['config']) && is_array($config['config']) ? $config['config'] : [];
$config['config']['name'] = $name;
if (empty($config['config']['name'])) {
$config['config']['name'] = $name;
}
}

return $configs;
Expand Down
2 changes: 1 addition & 1 deletion Config/TypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected function nameSection()
->ifTrue(function ($name) {
return !preg_match('/^[_a-z][_0-9a-z]*$/i', $name);
})
->thenInvalid('Invalid type name "%s". (see https://facebook.github.io/graphql/#Name)')
->thenInvalid('Invalid type name "%s". (see http://facebook.github.io/graphql/October2016/#Name)')
->end();

return $node;
Expand Down
10 changes: 4 additions & 6 deletions DependencyInjection/Compiler/ConfigTypesPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,16 @@ public function process(ContainerBuilder $container)
->compile(TypeGenerator::MODE_MAPPING_ONLY);

foreach ($generatedClasses as $class => $file) {
$aliases = [preg_replace('/Type$/', '', substr(strrchr($class, '\\'), 1))];
$this->setTypeServiceDefinition($container, $class, $aliases);
$alias = preg_replace('/Type$/', '', substr(strrchr($class, '\\'), 1));
$this->setTypeServiceDefinition($container, $class, $alias);
}
}

private function setTypeServiceDefinition(ContainerBuilder $container, $class, array $aliases)
private function setTypeServiceDefinition(ContainerBuilder $container, $class, $alias)
{
$definition = $container->setDefinition($class, new Definition($class));
$definition->setPublic(false);
$definition->setArguments([new Reference(ConfigProcessor::class), new Reference(GlobalVariables::class)]);
foreach ($aliases as $alias) {
$definition->addTag(TypeTaggedServiceMappingPass::TAG_NAME, ['alias' => $alias, 'generated' => true]);
}
$definition->addTag(TypeTaggedServiceMappingPass::TAG_NAME, ['alias' => $alias, 'generated' => true]);
}
}
102 changes: 66 additions & 36 deletions DependencyInjection/Compiler/TaggedServiceMappingPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

abstract class TaggedServiceMappingPass implements CompilerPassInterface
Expand All @@ -18,23 +19,22 @@ private function getTaggedServiceMapping(ContainerBuilder $container, $tagName)

foreach ($taggedServices as $id => $tags) {
$className = $container->findDefinition($id)->getClass();
foreach ($tags as $tag) {
$this->checkRequirements($id, $tag);
$tag = array_merge($tag, ['id' => $id]);
if (!$isType) {
$tag['method'] = isset($tag['method']) ? $tag['method'] : '__invoke';
foreach ($tags as $attributes) {
$this->checkRequirements($id, $attributes);
$attributes = self::resolveAttributes($attributes, $id, !$isType);
$solutionID = $className;

if (!$isType && '__invoke' !== $attributes['method']) {
$solutionID = sprintf('%s::%s', $className, $attributes['method']);
}
if (isset($tag['alias'])) {
$serviceMapping[$tag['alias']] = $tag;

if (!isset($serviceMapping[$solutionID])) {
$serviceMapping[$solutionID] = $attributes;
}

// add FQCN alias
$alias = $className;
if (!$isType && '__invoke' !== $tag['method']) {
$alias .= '::'.$tag['method'];
if (isset($attributes['alias']) && $solutionID !== $attributes['alias']) {
$serviceMapping[$solutionID]['aliases'][] = $attributes['alias'];
}
$tag['alias'] = $alias;
$serviceMapping[$tag['alias']] = $tag;
}
}

Expand All @@ -46,35 +46,19 @@ public function process(ContainerBuilder $container)
$mapping = $this->getTaggedServiceMapping($container, $this->getTagName());
$resolverDefinition = $container->findDefinition($this->getResolverServiceID());

foreach ($mapping as $name => $options) {
$cleanOptions = $options;
$solutionID = $options['id'];
foreach ($mapping as $solutionID => $attributes) {
$attributes['aliases'] = array_unique($attributes['aliases']);
$aliases = $attributes['aliases'];
$serviceID = $attributes['id'];

$solutionDefinition = $container->findDefinition($solutionID);
$solutionDefinition = $container->findDefinition($serviceID);
// make solution service public to improve lazy loading
$solutionDefinition->setPublic(true);

$methods = array_map(
function ($methodCall) {
return $methodCall[0];
},
$solutionDefinition->getMethodCalls()
);
if (
empty($options['generated']) // false is consider as empty
&& is_subclass_of($solutionDefinition->getClass(), ContainerAwareInterface::class)
&& !in_array('setContainer', $methods)
) {
@trigger_error(
'Autowire custom tagged (type, resolver or mutation) services is deprecated as of 0.9 and will be removed in 1.0. Use AutoMapping or set it manually instead.',
E_USER_DEPRECATED
);
$solutionDefinition->addMethodCall('setContainer', [new Reference('service_container')]);
}
$this->autowireSolutionImplementingContainerAwareInterface($solutionDefinition, empty($attributes['generated']));

$resolverDefinition->addMethodCall(
'addSolution',
[$name, [new Reference('service_container'), 'get'], [$solutionID], $cleanOptions]
[$solutionID, [[new Reference('service_container'), 'get'], [$serviceID]], $aliases, $attributes]
);
}
}
Expand All @@ -88,6 +72,52 @@ protected function checkRequirements($id, array $tag)
}
}

/**
* @param array $attributes
* @param string $id
* @param bool $withMethod
*
* @return array
*/
private static function resolveAttributes(array $attributes, $id, $withMethod)
{
$default = ['id' => $id, 'aliases' => []];
if ($withMethod) {
$default['method'] = '__invoke';
}
$attributes = array_replace($default, $attributes);

return $attributes;
}

/**
* @param Definition $solutionDefinition
* @param bool $isGenerated
*/
private function autowireSolutionImplementingContainerAwareInterface(Definition $solutionDefinition, $isGenerated)
{
$methods = array_map(
function ($methodCall) {
return $methodCall[0];
},
$solutionDefinition->getMethodCalls()
);
if (
$isGenerated
&& is_subclass_of($solutionDefinition->getClass(), ContainerAwareInterface::class)
&& !in_array('setContainer', $methods)
) {
@trigger_error(
sprintf(
'Autowire method "%s::setContainer" for custom tagged (type, resolver or mutation) services is deprecated as of 0.9 and will be removed in 1.0.',
ContainerAwareInterface::class
),
E_USER_DEPRECATED
);
$solutionDefinition->addMethodCall('setContainer', [new Reference('service_container')]);
}
}

abstract protected function getTagName();

/**
Expand Down
89 changes: 63 additions & 26 deletions Resolver/AbstractResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,51 @@ abstract class AbstractResolver implements FluentResolverInterface
/** @var array */
private $solutions = [];

private $aliases = [];

/** @var array */
private $solutionOptions = [];

/** @var array */
private $fullyLoadedSolutions = [];

public function addSolution($name, callable $solutionFunc, array $solutionFuncArgs = [], array $options = [])
public function addSolution($id, $solutionOrFactory, array $aliases = [], array $options = [])
{
$this->fullyLoadedSolutions[$name] = false;
$this->solutions[$name] = function () use ($name, $solutionFunc, $solutionFuncArgs) {
$solution = call_user_func_array($solutionFunc, $solutionFuncArgs);
$this->checkSolution($name, $solution);
$this->fullyLoadedSolutions[$id] = false;
$this->addAliases($id, $aliases);

$this->solutions[$id] = function () use ($id, $solutionOrFactory) {
$solution = $solutionOrFactory;
if (self::isSolutionFactory($solutionOrFactory)) {
if (!isset($solutionOrFactory[1])) {
$solutionOrFactory[1] = [];
}
$solution = call_user_func_array(...$solutionOrFactory);
}
$this->checkSolution($id, $solution);

return $solution;
};
$this->solutionOptions[$name] = $options;
$this->solutionOptions[$id] = $options;

return $this;
}

public function hasSolution($name)
public function hasSolution($id)
{
return isset($this->solutions[$name]);
$id = $this->resolveAlias($id);

return isset($this->solutions[$id]);
}

/**
* @param $name
* @param $id
*
* @return mixed
*/
public function getSolution($name)
public function getSolution($id)
{
return $this->loadSolution($name);
return $this->loadSolution($id);
}

/**
Expand All @@ -50,38 +62,63 @@ public function getSolutions()
return $this->loadSolutions();
}

public function getSolutionAliases($id)
{
return array_keys($this->aliases, $id);
}

/**
* @param $name
* @param $id
*
* @return mixed
*/
public function getSolutionOptions($name)
public function getSolutionOptions($id)
{
return isset($this->solutionOptions[$name]) ? $this->solutionOptions[$name] : [];
$id = $this->resolveAlias($id);

return isset($this->solutionOptions[$id]) ? $this->solutionOptions[$id] : [];
}

/**
* @param string $name
* @param string $id
*
* @return mixed
*/
private function loadSolution($name)
private function loadSolution($id)
{
if (!$this->hasSolution($name)) {
$id = $this->resolveAlias($id);
if (!$this->hasSolution($id)) {
return null;
}

if ($this->fullyLoadedSolutions[$name]) {
return $this->solutions[$name];
if ($this->fullyLoadedSolutions[$id]) {
return $this->solutions[$id];
} else {
$loader = $this->solutions[$name];
$this->solutions[$name] = $loader();
$this->fullyLoadedSolutions[$name] = true;
$loader = $this->solutions[$id];
$this->solutions[$id] = $loader();
$this->fullyLoadedSolutions[$id] = true;

return $this->solutions[$id];
}
}

return $this->solutions[$name];
private function addAliases($id, $aliases)
{
foreach ($aliases as $alias) {
$this->aliases[$alias] = $id;
}
}

private static function isSolutionFactory($solutionOrFactory)
{
return is_array($solutionOrFactory) && isset($solutionOrFactory[0]) && is_callable($solutionOrFactory[0]);
}

private function resolveAlias($alias)
{
return isset($this->aliases[$alias]) ? $this->aliases[$alias] : $alias;
}

/**
* @return mixed[]
*/
Expand All @@ -106,11 +143,11 @@ protected function supportsSolution($solution)
return null === $supportedClass || $solution instanceof $supportedClass;
}

protected function checkSolution($name, $solution)
protected function checkSolution($id, $solution)
{
if (!$this->supportsSolution($solution)) {
throw new UnsupportedResolverException(
sprintf('Resolver "%s" must be "%s" "%s" given.', $name, $this->supportedSolutionClass(), get_class($solution))
sprintf('Resolver "%s" must be "%s" "%s" given.', $id, $this->supportedSolutionClass(), get_class($solution))
);
}
}
Expand All @@ -122,6 +159,6 @@ protected function checkSolution($name, $solution)
*/
protected function supportedSolutionClass()
{
return;
return null;
}
}
Loading

0 comments on commit 10b1660

Please sign in to comment.