Symfony call a reusable bundle like a service - symfony

I'm trying to create a reusable bundle to manage basket in webstore application...
I create every desired action in a reusable bundle using a controller like addBasketAction, removeBasketAction and so on... The classical
Everythings works fine, but now I'm litteraly stuck about how to "call" my bundle from my "principal" application

You dont call a bundle but you have to register your bundle in the AppKernel.php file:
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
// ...
new AppBundle\AppBundle(),
new Company\CustomBundle\CompanyCustomBundle(),
];
}
// ...
}
Then you will need to import your routes too but i dont know if you use annotations for your routes or a yaml file.
read more:
http://symfony.com/doc/current/bundles.html
http://symfony.com/doc/current/bundles/best_practices.html
[edit]
Reading your topic title it looks like that you are confusing bundles and services. You can add a service in your bundle on the same way as you can add this service in your AppBundle:
http://symfony.com/doc/current/service_container.html#creating-configuring-services-in-the-container

Related

Load Symfony (5.2) config from database

I am a newbie in Symfony but I know how to use OOP in PHP.
I try (with frustration) to couple custom parameters with Symfony configs by using Doctrine entities.
To solve the problem I used for e.g. the answer from Michael Sivolobov: https://stackoverflow.com/a/28726681/2114615 and other sources.
My solution:
Step 1: Create new package in config folder
-> config
-> packages
-> project
-> services.yaml
-> project
-> src
-> ParameterLoaderBundle.php
-> DependencyInjection
-> Compiler
-> ParameterLoaderPass.php
Step 2: Import the new resource
# config/services.yaml
...
imports:
- { resource: 'packages/project/config/services.yaml' }
...
Step 3: Package coding
# packages/project/config/services.yaml
services:
Project\:
resource: "../src"
<?php
namespace Project;
use Project\DependencyInjection\Compiler\ParameterLoaderPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ParameterLoaderBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ParameterLoaderPass(), PassConfig::TYPE_AFTER_REMOVING);
}
}
<?php
namespace Project\DependencyInjection\Compiler;
use App\Entity\SettingCategory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ParameterLoaderPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$em = $container->get('doctrine.orm.default_entity_manager');
$setting = $em->getRepository(SettingCategory::class)->findAll();
$container->setParameter('test', $setting);
}
}
After at all I test the new Parameter in my API controller:
$this->getParameter('Test');
But the following error message appears:
The parameter \"test\" must be defined.
Couple of things going on here. First off, loading config from a database is very unusual in Symfony so it is not surprising that you are having difficulty. Secondly, your process code is never getting called. Part of debugging is making sure that code that you expect to be called is in fact being called. Third, you really got off on a tangent with attempting to add a bundle under config. Way back in Symfony 2 there used to be more bundle related stuff under app/config and it may be that you discovered some old articles and misunderstood them.
But, the big problem here is that Symfony has what is known as a 'compile' phase which basically processes all the configuration and caches it. Hence the CompilerPassInterface. Unfortunately, services themselves are not available during the compile phase. They simply don't exist yet so no entity manager. You need to open your own database connection if you really want to load config from a database. You will want to use just a database connection object and not the entity manager since part of the compile phase is to process the entities themselves.
So get rid of all your code and just adjust your Kernel class:
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
{
use MicroKernelTrait;
public function process(ContainerBuilder $container)
{
$url = $_ENV['DATABASE_URL'];
$conn = DriverManager::getConnection(['url' => $url]);
$settings = $conn->executeQuery('SELECT * FROM settings')->fetchAllAssociative();
$container->setParameter('test',$settings);
}
And be aware that even if you get all this working, you will need to manually rebuild the Symfony cache after updating your settings table. It is not going to be automatic. You really might consider taking a completely different approach.

Getting a list of tagged services in my controller

What i want is to add services to the service container that i want to use later in my controller or service.
So i created two services with my custom tag fbeen.admin
here they are:
services:
app.test:
class: AppBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
fbeen.admin.test:
class: Fbeen\AdminBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
Now i want to use all the services with the tag fbeen.admin in my controller but i dont know how.
I followed the How to work with service tags tutorial but i get stuck on this rule:
$definition->addMethodCall('addTransport', array(new Reference($id)));
On some way the addTransport method of the TransportChain class should be called but it seems that it isn't been called.
And even if it would be called then i still do not have a list of services with the fbeen.admin tag into my controller.
I am sure that i am missing something but who can explain me what it is?
p.s. I know that compilerPass runs at buildtime but for example sonata admin knows all admin classes and twig knows all twig extensions. How do they know?
Thank you for reading this :-)
Symfony 3.3
Container gets compiled once (in debug more often, but in production only once). What you manage with addMethodCall... is that once you request your service from container, which you are storing in $definition (that in this case is controller). Then container will call method addMethodCall('method'.. during initialising your service.
What it will look in container:
// This is pseudo content of compiled container
$service = new MyController();
// This is what compiler pass addMethodCall will add, now its your
// responsibility to implement method addAdmin to store admins in for
// example class variable. This is as well way which sonata is using
$service->addAdmin(new AppBundle\Admin\TestAdmin());
$service->addAdmin(new AppBundle\Admin\TestAdmin());
return $service; // So you get fully initialized service
Symfony 3.4+
What you can do is this:
// Your services.yaml
services:
App/MyController/WantToInjectSerivcesController:
arguments:
$admins: !tagged fbeen.admin
// Your controller
class WantToInjectSerivcesController {
public function __construct(iterable $admins) {
foreach ($admins as $admin) {
// you hot your services here
}
}
}
Bonus autotagging of your services. Lets say all your controllers implements interface AdminInterface.
// In your extension where you building container or your kernel build method
$container->registerForAutoconfiguration(AdminInterface::class)->addTag('fbeen.admin');
This will tag automatically all services which implement your interface with tag. So you don't need to set tag explicitly.
The thing to note here is this: The CompilerPass doesn't run the 'addTransport' (or whatever you may call it) in the compiler-pass itself - just says 'when the time is right - run $definition->addTransport(...) class, with this data'. The place to look for where that happens is in your cache directory (grep -R TransportChain var/cache/), where it sets up the $transportChain->addTransport(...).
When you come to use that service for the first time - only then is the data filled in as the class is being instantiated from the container.
This worked for me:
extend the TransportChain class with a getTransports method:
public function getTransports()
{
return $this->transports;
}
and use the TransportChain service in my controller:
use AppBundle\Mail\TransportChain;
$transportChain = $this->get(TransportChain::class);
$transports = $transportChain->getTransports();
// $transports is now an array with all the tagged services
Thank you Alister Bulman for pushing me forwards :-)

upgrade symfony3.4 with bundles extension to symfony4 flex

I have a project in symfony 3.4 then I tried to upgrade to symfony 4 /flex.
the problem is in my code there a 3 bundles TBAdminBundle, TBPlatformBundle and TBSecurityBundle.
Also for each bundle there Extension class exemple.
class TBPlatformExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new PlatformConfiguration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
if (!in_array(strtolower($config['db_driver']), array('custom', 'mongodb', 'orm'))) {
throw new \InvalidArgumentException(sprintf('Invalid db driver "%s".', $config['db_driver']));
}
if ('custom' !== $config['db_driver']) {
$loader->load(sprintf('%s.yml', $config['db_driver']));
$def = new Definition('Doctrine\ORM\EntityManager', array('%tb_notification.model_manager_name%'));
$def->setPublic(false);
if (method_exists($def, 'setFactory')) {
$def->setFactory(array(new Reference('doctrine'), 'getManager'));
} else {
// To be removed when dependency on Symfony DependencyInjection is bumped to 2.6
$def->setFactoryService('doctrine');
$def->setFactoryMethod('getManager');
}
$container->setDefinition('tb_notification.entity_manager', $def);
}
foreach (array('form', 'command', 'events') as $basename) {
$loader->load(sprintf('%s.yml', $basename));
}
$container->setParameter('tb_notification.model_manager_name', $config['model_manager_name']);
$container->setParameter('tb_notification.form.notification.type', $config['form']['notification']['type']);
$container->setParameter('tb_notification.form.notification.name', $config['form']['notification']['name']);
$container->setParameter('tb_trip.form.trip.type', $config['form']['trip']['type']);
$container->setParameter('tb_trip.form.trip.name', $config['form']['trip']['name']);
$container->setParameter(
'tb_notification.form.delete_notification.type',
$config['form']['delete_notification']['type']
);
$container->setParameter(
'tb_notification.form.delete_notification.name',
$config['form']['delete_notification']['name']
);
$container->setParameter('tb_trip.form.trip.update_trip.type', $config['form']['update_trip']['type']);
$container->setParameter('tb_trip.form.trip.update_trip.name', $config['form']['update_trip']['name']);
$container->setParameter('tb_action.form.action.type', $config['form']['action']['type']);
$container->setParameter('tb_action.form.action.name', $config['form']['action']['name']);
$container->setParameter('tb_place.form.place.type', $config['form']['place']['type']);
$container->setParameter('tb_place.form.place.name', $config['form']['place']['name']);
$container->setParameter('tb_action_privacy.form.action.type', $config['form']['privacy']['type']);
$container->setParameter('tb_action_privacy.form.action.name', $config['form']['privacy']['name']);
$container->setParameter('tb_notification.model.notification.class', $config['class']['model']['notification']);
$container->setParameter('tb_trip.model.trip.class', $config['class']['model']['trip']);
$container->setParameter('tb_like.model.like.class', $config['class']['model']['like']);
$container->setParameter('tb_helpful.model.helpful.class', $config['class']['model']['helpful']);
$container->setParameter('tb_action.model.action.class', $config['class']['model']['action']);
$container->setParameter('tb_budget.model.class', $config['class']['model']['budget']);
$container->setParameter('tb_action.model.action_privacy_policy.class', $config['class']['model']['privacy']);
$container->setParameter('tb_rating.model.rating.class', $config['class']['model']['vote']);
$container->setParameter('tb_place.model.place.class', $config['class']['model']['place']);
$container->setParameter('tb_destination.model.destination.class', $config['class']['model']['destination']);
// parameters for hydrating object with doctrine
$container->setParameter('tb_action.hydrate.action', $config['hydrate']['action']);
$container->setParameter('tb_google.key', $config['google']['key']);
$container->setParameter('compare_text.default_percent', $config['compare']['text']);
$container->setAlias('tb_trip.manager.trip', $config['service']['manager']['trip']);
$container->setAlias('tb_like.manager.post', $config['service']['manager']['like']);
$container->setAlias('tb_helpful.manager.post', $config['service']['manager']['helpful']);
$container->setAlias('tb_budget_manager', $config['service']['manager']['budget']);
$container->setAlias('tb_place_manager', $config['service']['manager']['place']);
$container->setAlias('tb_destination_manager', $config['service']['manager']['destination']);
$container->setAlias('tb_compare_string', 'similar_text.manager');
$container->setAlias('foursquare.manager', 'foursquare.manager.default');
$container->setAlias('tb_action.form_factory', $config['service']['form_factory']['action']);
$container->setAlias('tb_place.form_factory', $config['service']['form_factory']['place']);
$container->setAlias('tb_action_privacy.form_factory', $config['service']['form_factory']['privacy']);
$container->setAlias(
'tb_notification.form_factory.notification',
$config['service']['form_factory']['notification']
);
$container->setAlias('tb_trip.form_factory', $config['service']['form_factory']['trip']);
$container->setAlias(
'tb_notification.form_factory.notification_delete',
$config['service']['form_factory']['delete_notification']
);
$container->setAlias('tb_action.customer_repository', 'tb_action.customer_repository_default');
$container->setAlias('tb_notification.customer_repository', 'tb_notification.customer_repository_default');
$container->setAlias('tb_trip.customer_repository', 'tb_trip.customer_repository_default');
}
}
According to documentation
"In Symfony versions prior to 4.0, it was recommended to organize your own application code using bundles. This is no longer recommended and bundles should only be used to share code and features between multiple applications."
I know this it's no more possible with symfony4/flex
How can I rewrite this to match with flex configuration?
thanks
My suggestion is to migrate progressively your bundles.
1. Put the bundle in the src of your SF4 application like this :
src/
Bundle/
TBAdminBundle
TBPlatformBundle
TBSecurityBundle
Command
Controller
Entity
...
2. Use Composer PSR-4 to autoload them (composer.json)
"autoload" : {
"psr-4" : {
"App\\" : "src/",
"TBAdminBundle\\" : "src/Bundle/TBAdminBundle/",
"TBPlatformBundle\\" : "src/Bundle/TBPlatformBundle/",
"TBSecurityBundle\\" : "src/Bundle/TBSecurityBundle/",
...
3. Exclude them from App Services autoload (config/services.yaml)
App\:
resource: '../src/*'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../src/{Entity,Migrations,Repository,Bundle}'
4. Validate the new installation/configuration
Although SF4 is bundle less for core App application, it supports bundle ... Just check that you resolve all deprecated functions from those bundles
5. Start progressive migration of your bundles code:
As Hint :
Rework your config/parameters to use ENV, and services.yaml parameters
So you can transfer most config variables/parameters to the App level and easily share them ...
All your services definitions can be moved from Extention classes into services.yaml for simplicity and easy maintenance.
You will degrease bundles code as time go and you gain experience with the new SF4 services usability and orientations.
That note tells you that it is no longer a recommendation but it doesn't mean that you should refactor your code completely. If you already have your code packed as bundles you should only remove deprecated code. In general it is a good idea to pack your code in bundles only if it is reusable across projects.

Symfony2 Adding 3rd party Bundles to a Service Controller

I am looking to get some help on how to add a 3rd party bundle to an existing service controller, specifically KnpSnappyBundle https://github.com/KnpLabs/KnpSnappyBundle.
The bundle was installed with composer, and registerBundles() function in app/AppKernel.php was updated.
I am having trouble with passing the bundle to a service controller, via its constructor.
1: Unsure how to figure out the path to use
--SomeController.php file--
...
use Symfony\Component\???\???
class FormDataController
{
...
private $pdf;
2: Also unsure how to figure out what the object type is named.
public function __construct(..., KnpSnappyBundle? $pdf )
{
...
$this->pdf= $pdf;
return $this;
}
3: Then in the servives.yml file i add the following argument to the correct controller
- "#knp_snappy.pdf"
After doing the above, the controller should be able to do the following to access the bundles functions, correct?
$this->pdf->...
Why are you doing this?
Quote:
The bundle registers two services:
the knp_snappy.image service allows you to generate images;
the knp_snappy.pdf service allows you to generate pdf files.
Just access them like this (if you use standard Symfony controller):
$this->get('knp_snappy.pdf')->...;
$this->get('knp_snappy.image')->...;
UPDATE:
If you are injecting knp_snappy.pdf service into your custom service, you should use Knp\Bundle\SnappyBundle\Snappy\LoggableGenerator class.
--SomeController.php file--
...
use Knp\Bundle\SnappyBundle\Snappy\LoggableGenerator;
class FormDataController
{
// ... //
private $pdf;
public function __construct(..., LoggableGenerator $pdf )
{
...
$this->pdf = $pdf;
return $this;
}
// ... //
}
When you run ./app/console container:debug command, it lists all available services and corresponding classes. Very helpful.

Custom route configuration with Silex

I know that the basis of Silex approach in which all the application logic in a single file. But my application will be possible to have more than twenty controllers. So I want to have a handy map to manage the router.
My question is to search for solutions in which I would be able to make a router to a separate file. In the best case, the file must be of YAML type:
# config/routing.yml
_home:
pattern: /
defaults: { _controller: MyProject\Controller\MyController::index }
But the native is also a good case (for me):
$routes = new RouteCollection();
$routes->add(
'home',
new Route('/', array('controller' => 'MyProject\Controller\MyController::index')
));
return $routes;
Problem of the second case is that I have to use the match() function for each rule of routing. It is not at all clear.
What are the ways to solve this issue? The condition is that I want to use the existing API Silex or components of Symfony2.
Small note:
I don't use a ControllerProviderInterface for my Controller classes. This is an independent classes.
First of all, the basis of Silex is not that you put everything in one file. The basis of Silex is that you create your own 'framework', your own way of organizing applications.
"Use silex if you are comfortable with making all of your own architecture decisions and full stack Symfony2 if not."
-- Dustin Whittle
Read more about this in this blogpost, created by the creator of Silex.
How to solve your problem
What you basically want is to parse a Yaml file and get the pattern and defaults._controller settings from each route that is parsed.
To parse a Yaml file, you can use the Yaml Component of Symfony2. You get an array back which you can use to add the route to Silex:
// parse the yaml file
$routes = ...;
$app = new Silex\Application();
foreach ($routes as $route) {
$app->match($route['pattern'], $route['defaults']['_controller']);
}
// ...
$app->run();
I thought I'd add my method here as, although others may work, there isn't really a simple solution. Adding FileLocator / YamlFileLoader adds a load of bulk that I don't want in my application just to read / parse a yaml file.
Composer
First, you're going to need to include the relevant files. The symfony YAML component, and a really simple and useful config service provider by someone who actively works on Silex.
"require": {
"symfony/yaml": "~2.3",
"igorw/config-service-provider": "1.2.*"
}
File
Let's say that your routes file looks like this (routes.yml):
config.routes:
dashboard:
pattern: /
defaults: { _controller: 'IndexController::indexAction' }
method: GET
Registration
Individually register each yaml file. The first key in the file is the name it will be available under your $app variable (handled by the pimple service locator).
$this->register(new ConfigServiceProvider(__DIR__."/../config/services.yml"));
$this->register(new ConfigServiceProvider(__DIR__."/../config/routes.yml"));
// any more yaml files you like
Routes
You can get these routes using the following:
$routes = $app['config.routes']; // See the first key in the yaml file for this name
foreach ($routes as $name => $route)
{
$app->match($route['pattern'], $route['defaults']['_controller'])->bind($name)->method(isset($route['method'])?$route['method']:'GET');
}
->bind() allows you to 'name' your urls to be used within twig, for example.
->method() allows you to specify POST | GET. You'll note that I defaulted it to 'GET' with a ternary there if the route doesn't specify a method.
Ok, that's how I solved it.
This method is part of my application and called before run():
# /src/Application.php
...
protected function _initRoutes()
{
$locator = new FileLocator(__DIR__.'/config');
$loader = new YamlFileLoader($locator);
$this['routes'] = $loader->load('routes.yml');
}
Application class is my own and it extends Silex\Application.
Configuration file:
# /src/config/routes.yml
home:
pattern: /
defaults: { _controller: '\MyDemoSite\Controllers\DefaultController::indexAction' }
It works fine for me!
UPD:
I think this is the right option to add collections:
$this['routes']->addCollection($loader->load('routes.yml'));
More flexible.
You could extend the routes service (which is a RouteCollection), and load a YAML file with FileLocator and YamlFileLoader:
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
$app->extend('routes', function($routeCollection) {
$locator = new FileLocator([__DIR__ . '/../config']);
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');
$routeCollection->addCollection($collection);
return $routeCollection;
});
You will need symfony/config and symfony/yaml dependencies though.

Resources