Skip to content

Instantly share code, notes, and snippets.

@spiechu
Last active September 21, 2015 20:46
Show Gist options
  • Save spiechu/2b11484ca25d37829aa1 to your computer and use it in GitHub Desktop.
Save spiechu/2b11484ca25d37829aa1 to your computer and use it in GitHub Desktop.
<?php
namespace MyProject\MyBundle;
class AsciiConverter implements StringProcessorInterface
{
/**
* {@inheritdoc}
*/
public function getProccessedString($string) {
return iconv(mb_detect_encoding($string), 'ASCII//TRANSLIT', $string);
}
}
<?php
namespace MyProject\MyBundle\Controller;
use MyProject\MyBundle\ProcessorService;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class HomepageController extends Controller
{
/**
* @return Response
*/
public function homepageAction()
{
/* @var $processorService ProcessorService */
$processorService = $this->get('my_domain.processor.service');
$string = ' Chrząszcz brzmi w trzcinie, ąę ';
$processedString = $processorService->processString($string);
// $processed string contains
// CHRZASZCZ BRZMI W TRZCINIE, AE
// all three processors were run
return $this->render('MyProjectMyBundle:Homepage:homepage.html.twig');
}
}
<?php
namespace MyProject\MyBundle;
use MyProject\MyBundle\DependencyInjection\Compiler\ProcessorCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class MyProjectMyBundle extends Bundle
{
/**
* {@inheritdoc}
*/
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(
new ProcessorCompilerPass(),
// this way every services' parameters are resolved
// and we have proper class names in compiler
PassConfig::TYPE_OPTIMIZE
);
}
}
<?php
namespace MyProject\MyBundle\DependencyInjection\Compiler;
use MyProject\MyBundle\StringProcessorInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
class ProcessorCompilerPass implements CompilerPassInterface
{
const COLLECTION_ID = 'my_domain.processor.collection';
const PROCESSOR_TAG_NAME = 'my_project.processor.provider';
const TAG_ARG_PRIORITY = 'priority';
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException When tagged class doesn't
* implement processor interface
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition(self::COLLECTION_ID)) {
return;
}
$collectionDefinition = $container->getDefinition(self::COLLECTION_ID);
$taggedServiceConfigs = $container->findTaggedServiceIds(self::PROCESSOR_TAG_NAME);
$serviceDefinitions = new \SplPriorityQueue();
foreach ($taggedServiceConfigs as $serviceId => $tags) {
$definition = $container->getDefinition($serviceId);
// compilation will fail
// we'll have knowledge what service is messed up even before application runs
$this->assertStringProcessor($definition);
// we could have multiple processor tags,
// for example one processor call with a high priority
// and the same processor call at the end with low priority
foreach ($tags as $tag) {
$priority = isset($tag[self::TAG_ARG_PRIORITY])
? (int) $tag[self::TAG_ARG_PRIORITY]
: 10;
$serviceDefinitions->insert($definition, $priority);
}
}
$collectionDefinition->setArguments([
// this way we have sorted definitions by priorities
iterator_to_array($serviceDefinitions),
]);
}
/**
* @param Definition $definition
*
* @throws InvalidArgumentException When tagged class doesn't
* implement processor interface
*/
private function assertStringProcessor(Definition $definition) {
if (!is_a($definition->getClass(), StringProcessorInterface::class, true)) {
throw new InvalidArgumentException(sprintf(
'Class "%s" does not implement "%s" interface',
$definition->getClass(),
StringProcessorInterface::class
));
}
}
}
<?php
namespace MyProject\MyBundle;
class ProcessorService
{
/**
* @var StringProcessorInterface[]
*/
private $processors = [];
/**
* @param array|\Traversable $processors
*
* @throws InvalidArgumentException When processor is not an array
* or object implementing \Traversable interface
*/
public function __construct($processors)
{
// we cannot have clear typehint to traversable
// since array doesn't implement \Traversable ;-)
if (!is_array($processors) && !$processors instanceof \Traversable) {
throw new InvalidArgumentException(
'Processors must be array or implement \Traversable interface'
);
}
// super defensive programming in action
foreach ($processors as $processor) {
$this->addProcessor($processor);
}
}
/**
* @param StringProcessorInterface $processor
*/
private function addProcessor(StringProcessorInterface $processor) {
$this->processors[] = $processor;
}
/**
* @param string $string
*
* @return string
*/
public function processString($string) {
foreach ($this->processors as $processor) {
// maybe catch exceptions,
// log them and do not kill whole processing?
$string = $processor->getProccessedString($string);
}
return $string;
}
}
parameters:
# we'll check if class name in parameter will be properly resolved by compiler pass
other_bundle.processor.upcase.class: 'MyProject\MyBundle\Upcase'
services:
# main processing service
my_domain.processor.service:
class: MyProject\MyBundle\ProcessorService
arguments:
# we're only interested in whole processors collection
- @my_domain.processor.collection
# collection filled by compiler pass
my_domain.processor.collection:
# dirty trick to have an undercover array ;-)
class: ArrayObject
# cannot be accessed by $container->get()
public: false
# tagged string processor discovered with compiler pass and added into collection
my_domain.processor.upcase:
# first stage of compilation will contain unresolved parameters
# that's why we have compiler pass on PassConfig::TYPE_OPTIMIZE
class: %other_bundle.processor.upcase.class%
public: false
tags:
# no priority given, defaults to 10
- { name: my_project.processor.provider }
my_domain.processor.ascii:
class: MyProject\MyBundle\AsciiConverter
public: false
tags:
# low priority, processed as last
- { name: my_project.processor.provider, priority: 1 }
# --- suppose this is other bundle with other services.yml definition
# other bundle defines processor with higher priority
other_bundle.processor.trim:
class: MyProject\MyBundle\Trim
public: false
tags:
- { name: my_project.processor.provider, priority: 15 }
<?php
namespace MyProject\MyBundle;
interface StringProcessorInterface
{
/**
* @param string $string
*
* @return string
*/
public function getProccessedString($string);
}
<?php
namespace MyProject\MyBundle;
class Trim implements StringProcessorInterface
{
/**
* {@inheritdoc}
*/
public function getProccessedString($string) {
return trim($string);
}
}
<?php
namespace MyProject\MyBundle;
class Upcase implements StringProcessorInterface
{
/**
* {@inheritdoc}
*/
public function getProccessedString($string) {
return mb_strtoupper($string);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment