How to test the new pattern using AbstractBundle from Symfony6.1? - symfony

Since Symfony 6.1, instead of creating an extension class and another configuration class, our bundle class can now extend AbstractBundle to add this logic to the bundle class directly. This abstract class provides configuration hooks.
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
class AcmeSocialBundle extends AbstractBundle
{
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->arrayNode('twitter')
->children()
->integerNode('client_id')->end()
->scalarNode('client_secret')->end()
->end()
->end() // twitter
->end()
;
}
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
// Contrary to the Extension class, the "$config" variable is already merged
// and processed. You can use it directly to configure the service container.
$container->services()
->get('acme.social.twitter_client')
->arg(0, $config['twitter']['client_id'])
->arg(1, $config['twitter']['client_secret'])
;
}
}
Symfony documentation explains how to test no more used configuration class.
class ConfigurationTest extends TestCase
{
public function testEmptyConfiguration(): void
{
$config = Yaml::parse('');
$processor = new Processor();
$result = $processor->processConfiguration(
new Configuration(),
[$config]
);
self::assertSame($result, ['foo' => 'bar'],
]);
}
But how to test my $definition in the configure method of this new pattern?

In this case, we need to find a way to access the bundle configuration so that we can process and test it. There is usually a fixed configuration class behind the new structure (Symfony\Component\Config\Definition\Configuration), so the only thing you would need to do is retrieve it:
class ConfigurationTest extends TestCase
{
public function testEmptyConfig(): void
{
$configuration = (new AcmeSocialBundle())
->getContainerExtension()
->getConfiguration([], new ContainerBuilder(new ParameterBag()))
;
$configs = [Yaml::parse('...')];
$result = (new Processor())->processConfiguration($configuration, $configs);
self::assertSame(['foo' => 'bar'], $result);
}
}
Note that the $configs variable should be an array of arrays (array of configs).

Related

Is it possible to use registerForAutoconfiguration inside CompilerPass?

I am trying to simplify my applications dependency injection by creating a base injection class.
So far most of the code works fine, except for registerForAutoconfiguration
Here is the relevant code:
abstract class AbstractTaggedPass implements CompilerPassInterface
{
protected $interfaceClass;
protected $serviceClass;
protected $tag;
protected $method;
public function process(ContainerBuilder $container)
{
// always first check if the primary service is defined
if (!$container->has($this->serviceClass)) {
return;
}
// Register classes implementing the interface with tag
$container->registerForAutoconfiguration($this->interfaceClass)->addTag($this->tag); // Does not work
$definition = $container->findDefinition($this->serviceClass);
// find all service IDs with the tag
$taggedServices = $container->findTaggedServiceIds($this->tag);
foreach ($taggedServices as $id => $tags) {
foreach ($tags as $attributes) {
$definition->addMethodCall($this->method, [new Reference($id)]);
}
}
}
}
class SubscriptionPaymentProviderPass extends AbstractTaggedPass
{
protected $interfaceClass = SubscriptionPaymentProviderInterface::class
protected $serviceClass = SubscriptionPaymentProviderPool::class;
protected $tag = 'subscription.payment_provider';
protected $method = 'addProvider';
}
class SubscriptionBundle extends Bundle
{
protected function getContainerExtensionClass()
{
return SubscriptionExtension::class;
}
public function build(ContainerBuilder $container)
{
parent::build($container);
//$container->registerForAutoconfiguration(SubscriptionPaymentProviderInterface::class)->addTag('subscription.payment_provider');
$container->addCompilerPass(new SubscriptionPaymentProviderPass());
}
}
If I move registerForAutoconfiguration line from Bundle class into the CompilerPass class, then it no longer registers Services with the correct tag.
Is it possible to use it inside a compiler pass?
Do I need to enable something to make it work?
Compiler Pass is used after service definitions are parsed (via configuration file or extensions).
I think the right place for do this, is into an Extension.

How to inject dependency using Factory Pattern in Symfony2

I have this situation
abstract class Importer {
const NW = 1;
public static function getInstance($type)
{
switch($type)
{
case(self::NW):
return new NWImporter();
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
and
class NWImporter extends Importer
{
public function import($nid)
{
//do some staff, create myObject and call the parent method to save it
parent::saveObject($myObject);
}
}
and I want to use them like this
$importer = Importer::getInstance(Importer::NW);
$importer->import($nid);
my question is: how to inject doctrine to be used in saveObject method?
thanks
You need to configure your importer as a symfony service :
services:
test.common.exporter:
# put the name space of your class
class: Test\CommonBundle\NWImporter
arguments: [ "#doctrine" ]
then in NWImporter define a constructor with a parameter that will have the doctrine instance
public function __construct($doctrine)
{
$this->doctrine= $doctrine;
}
with this solution you can avoid using a factory method as symfony does it for you but if you wanna to keep it, When you call $importer = Importer::getInstance(Importer::NW); from your controller you can inject the doctrine argument in your factory method :
abstract class Importer {
const NW = 1;
public static function getInstance($type, $doctrine)
{
switch($type)
{
case(self::NW):
return new NWImporter($doctrine);
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
then in your controller you should to do something like that :
$doctrine = $this->container->get('doctrine');
$importer = Importer::getInstance(Importer::NW, $doctrine);
$importer->import($nid);

Symfony doctrine entity manager injected in thread

I'm trying to use doctrine entity manager in a thread. I use a static scope as suggested here .
Class A is a symfony service and doctrine entity manager is injected in service.yml
class A extends \Thread{
static $em;
public function __construct($em)
{
self::$em = $em;
}
public function run(){
self::$em->doSomething(); //here em is null
}
}
How i can use entity manager correctly from a thread?
UPDATE:
As #Mjh suggested I can't share entity manager from threads. I can have an istance of em in every threads however but this is very inefficient.
A solution could be build a container threaded class shared between threads in which I'll store the entities that return from doctrine queries. The entities obviously will be detached from entity manager but I need only a read cache shared between threads.
UPDATE2:
See my first answer
Open issue: avoid to initialize for every thread a new environment
We have built a doctrine cache shared between thread extending a Thread Safe Stackable.
Warning some parts of code are semplified for demo purpose.
class work extends \Collectable{
protected $parameters;
public static $doctrine_mongodb;
public function __construct($parameters){
$this->parameters = $parameters;
}
public function run()
{
try{
$loader = require __DIR__.'/../../../../../../vendor/autoload.php';
static::$container = unserialize($this->worker->container);
static::$doctrine_mongodb = static::$container->get('doctrine_mongodb');
...
DO WORK
$dm = static::$doctrine_mongodb->getManager();
$repo = $dm->getRepository('Bundle:Document');
$ris = $this->worker->doctrinecache->FindOneBy($repo, array('key' => $val));
...
}catch(\Exception $e){}
}
}
NB: in work class we have the parallel execution of work code and there we can safely use doctrine common cache.
It's not the same to share entity manager because document are detached but for read purpose is good. If somebody need to manage entities can use merge doctrine method.
class SWorker extends \Worker{
public $env;
public $debug;
public $mongodb_cache_engine;
public function __construct( $env, $debug, $doctrinecache, $workParams){
$this->workParams = $work;
$this->env = $env;
$this->debug = $debug;
$this->doctrinecache = $doctrinecache ;
}
public function start($options = null){
return parent::start(PTHREADS_INHERIT_NONE);
}
public function run(){
require_once __DIR__.'/../../../../../../app/bootstrap.php.cache';
require_once __DIR__.'/../../../../../../app/AppKernel.php';
$kernel = new \AppKernel($this->env, $this->debug);
$kernel->loadClassCache();
$kernel->boot();
$this->container = serialize($kernel->getContainer());
}
}
In Sworker class we prepare symfony environment for thread. Tnx to svenpelster https://github.com/krakjoe/pthreads/issues/369 for that.
class doctrinecache extends \Stackable{
public function __call($MethodName, $arguments){
$repository = array_shift($arguments);
$documentName = $repository->getDocumentName();
$hash = $this->generateHash($MethodName, $documentName, $arguments);
return $this->cacheIO($hash, $repository, $MethodName, $arguments);
}
public function cacheIO($hash, $repository, $MethodName, $arguments){
$result = isset($this["{$hash}"])? $this["{$hash}"] : NULL;
if(!$result){
$result = call_user_func_array(array($repository, $MethodName), $arguments);
$this["{$hash}"] = $result;
}
return $result;
}
}
And finally
$doctrineCache = $this->kernel->get('doctrineCacheService');
$pool = new \Pool($workerNumber, SWorker::class, [$this->kernel->getEnvironment(), $this->kernel->isDebug(), $doctrineCache ,$workParams]);
while(current($works ))
{
$pool->submit(current($works ));
next($works);
}
$pool->shutdown();
while(current($works ))
{
$arrayResults[] = current($works )->getResults();
next($works);
}

How can I bind a Symfony config tree to an object

How would I go about binding a Symfony config tree to a class rather than returning an array?
Using Symfony\Component\Config\Definition\Processor returns an array.
In my case I want the config to be bound to a class so I can use methods to combine parts of the data.
Here is a simple example of my use case. I want the config bound to a class so I can use a method to join table.name and table.version together (my actual use case is more complex, but this is a simple example)
config.yml
db:
table:
name: some_table
version: v2
ConfigurationInterface
class DBConfiguration implements ConfigurationInterface
{
/**
* {#inheritDoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('db');
$rootNode
->children()
->arrayNode('table')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('version')->end()
->end()
->end()
;
return $treeBuilder;
}
}
Class I want to bind the config to
class DB
{
public $table;
public function __construct()
{
$this->table = new Table();
}
}
class Table
{
public $name;
public $version;
/**
* #return string
* Calculate the full table name.
*/
public function getTableName()
{
return $this->name.'-'.$this->version;
}
}
The Symfony Config component doesn't support that.
However, in a Symfony project, this is usually done at the container compile phase. In your bundle's Extension class, you will have access to the configuration tree of your bundle in array form.
You can then take this array and assign it to a service defined in the service container that will create your config object.
This is exactly how DoctrineBundle's configuration class is built:
Abstract services (for the configuration and the factory) are defined in dbal.xml
When loading DoctrineBundle's extension, an instance of the abstract config service is created for each defined connection.
An instance of the abstract factory service is created for each defined connection.
The options array is then passed to the abstract factory service along with the configuration
When creating an instance, the factory then does the necessary transformations.
As far as I know, Symfony has no native support for this, however, you could implement it yourself. You could use subset of Symfony Serializer Component in charge of deserialization, but I think it would be an overkill. Especially since I don't see any PublicPropertyDenormalizer, only GetSetMethodNormalizer (which is denormalizer too). Therefor you would have to either make your config objects have get/set methods or roll PublicPropertyDenormalizer on your own. Possible but it really seems like an overkill and doesn't look like helping much:
Symfony Serializer Component
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
class MyConfigObject implements Symfony\Component\Serializer\Normalizer\DenormalizableInterface
{
private $field1;
private $subobject;
public function getField1()
{
return $this->field1;
}
public function setField1($field1)
{
$this->field1 = $field1;
}
public function getSubobject()
{
return $this->subobject;
}
public function setSubobject(SubObject $subobject)
{
$this->subobject = $subobject;
}
public function denormalize(\Symfony\Component\Serializer\Normalizer\DenormalizerInterface $denormalizer, $data, $format = null, array $context = array())
{
$obj = new static();
$obj->setField1($data['field1']);
$obj->setSubobject($denormalizer->denormalize($data['subobject'], 'SubObject'));
return $obj;
}
}
class SubObject
{
private $subfield1;
public function getSubfield1()
{
return $this->subfield1;
}
public function setSubfield1($subfield1)
{
$this->subfield1 = $subfield1;
}
}
$normalizer = new \Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer();
$obj = (new MyConfigObject())->denormalize($normalizer, $array);
Native PHP Way
Imo this is a lot easier than above as Symfony Serializer wasn't really ment for that.
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
trait Denormalizable
{
public function fromArray($array)
{
foreach ($array as $property => $value) {
if (is_array($value)) {
if ($this->$property instanceof ArrayDenormalizableInterface) {
$this->$property->fromArray($value);
} else {
$this->$property = $value;
}
} else {
$this->$property = $value;
}
}
}
}
interface ArrayDenormalizableInterface
{
public function fromArray($array);
}
class MyConfigObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $field1;
public $subobject;
public function __construct()
{
$this->subobject = new SubObject();
}
}
class SubObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $subfield1;
}
$myConf = new MyConfigObject();
$myConf->fromArray($array);
Whatever way you choose, you can now just take array returned from symfony processor and turn it into a config object you need.

Symfony Factory class

I would like to use a ResultFactory class as a service in my Symfony 2 application:
My Result factory class will be responsible to create a BaseResult instance.
Depending on the type passed to the get factory method, the ResultFactory will create the right ResultObject.
Here's what could be the code:
class ResultFactory
{
protected $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
public function get($type, $param)
{
$instance = null;
switch ($type) {
case 'Type1':
$instance = new Type1Result($param);
break;
case 'Type2':
$instance = new Type2Result($param);
break;
}
return $instance;
}
}
My question is:
I would like to use a service in my ResultObject. How do i inject this service to my ResultObject?
Thanks!
You are not using your service inside a result object. your factory is generating the result object.
You can define your factory service in services.yml of your bundle as:
result.factory:
class: ResultFactory
arguments: ["#translator"]
And in your controller you can call the service:
$resultObject = $this->get('result_factory')->get($type, $param);
Also you have core example how to create factory service using symfony2 in [the docs].(http://symfony.com/doc/current/components/dependency_injection/factories.html)

Resources