I'm creating a PHP package that will reside in the /vendor directory and it needs to access the database using Doctrine. In my main Symfony 4.2 code, I can access the database by using autowiring in the constructors like this:
public function __construct(EntityManagerInterface $em)
However, this only works with Symfony and does not seem to work inside public bundles. Please note that I need to access the default database connection, I do not want to create a new connection if one already exists.
I am trying to create a package that will work both with and without Symfony, so what I really need is the raw PHP to get the current database connection (not a new connection) without all the autowiring magic.
If the package is running under Symfony, it should use the current Symfony database connection. If not, it should create its own connection.
For example, I can open a new connection like this:
$conn = \Doctrine\DBAL\DriverManager::getConnection($params, $config)
That works fine, but I need a way to get the current Symfony database connection (something like this):
$conn = \Doctrine\DBAL\DriverManager::getCurrentConnection()
Unfortunately, there is no such function and I can't figure out how to create my own version of it.
You should define the class as a service and inject the EntityManager as an argument:
# vendor/Acme/HelloBundle/Resources/Config/services.yml
services:
acme_hellp.service_id:
class: Acme\HelloBundle\Class
arguments: ['#doctrine.orm.entity_manager']
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
class AcmeHelloExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.xml');
}
}
Also, check this documentation on the Symfony site how to load your config files:
https://symfony.com/doc/current/bundles/extension.html
Let me understand your problem, are you looking for a package to share using the doctrine Entity Manager of Symfony? so let me share with you my experience.
That is possible, and there are no solution visible to read or watch, so you need to review some packages to understand how they resolve the problem, the package i review was on this link, this package provide a support not only doctrine, also others platforms like Mongo, so let me share with you how they made the solution.
First, you need to crete a package in a bitbucket or github, and made this package as "type": "symfony-bundle". i read the comments before and you are correct, you need to create a Configuration and Extension file to enable you packages, so in your extension file you need to implement a Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface this package will allow you make the magic. so you can write some lines of code similar of this and replace for your namespace:
<?php
namespace NameSpaceBundle\CoreBundle\DependencyInjection;
use Exception;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
/**
* Class NameSpaceBundleExtension Configurations of the Core
* #package NameSpaceBundle\CoreBundle\DependencyInjection
*/
class NameSpaceBundleExtension extends Extension implements PrependExtensionInterface
{
/**
* {#inheritdoc}
* #param array $configs
* #param ContainerBuilder $container
* #throws Exception
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
$loader->load('routing.yml');
$this->processConfiguration(new Configuration(), $configs);
}
/**
* {#inheritdoc}
* #param ContainerBuilder $container
*/
public function prepend(ContainerBuilder $container)
{
$configs = $container->getExtensionConfig($this->getAlias());
$this->processConfiguration(new Configuration(), $configs);
$doctrineConfig = [];
$doctrineConfig['orm']['mappings'][] = array(
'name' => 'NameSpaceBundle',
'is_bundle' => true,
'type' => 'annotation',
'prefix' => 'NameSpaceBundle\CoreBundle\Entity'
);
$container->prependExtensionConfig('doctrine', $doctrineConfig);
}
/**
* {#inheritdoc}
* #return string
*/
public function getAlias()
{
return 'bundelName_core';
}
}
Review your service.yaml allocated on Resouces/config and now you can inject not only EntityManagerInterface.
services:
_defaults:
autowire: false # Automatically injects dependencies in your services.
autoconfigure: false # Automatically registers your services as commands, event subscribers, etc.
public: false
NameSpaceBundle\CoreBundle\Manager\Company:
public: false
class: NameSpaceBundle\CoreBundle\Doctrine\CompanyManager
arguments:
- '#doctrine'
- '\NameSpaceBundle\CoreBundle\Entity\Company'
Also this aprouch is similar of some packages of FriendsOfSymfony and the package i tell you before.
PD. Sorry for my english is not my native language
After a couple days of trying to create a Symfony bundle to make things work the "official" way, I figured out a one-liner hack that gets the Symfony Doctrine connection and does what I need. It relies on the fact that $kernel is a Symfony global:
// Singleton to get Doctrine database connection
//
// If we are running on the legacy system, it will open a new database connection.
//
// If we are running under Symfony, it will use the existing Doctrine
// connection.
class Doctrine
{
static private $instance = null;
private $conn;
// The database connection is established in a private constructor
private function __construct()
{
global $kernel;
if (class_exists('\config')) {
// running on legacy system
$cfg = new \config();
//
// set up Doctrine connection
//
$params = [
'driver' => 'pdo_mysql',
'user' => $cfg->db_username,
'password' => $cfg->db_password,
'host' => $cfg->db_hostname,
'dbname' => $cfg->db_name,
];
$config = new \Doctrine\DBAL\Configuration();
$this->conn = \Doctrine\DBAL\DriverManager::getConnection($params, $config);
} else {
// running on Symfony system
$this->conn = $kernel->getContainer()->get('doctrine.orm.default_entity_manager')->getConnection();
}
}
static public function getInstance()
{
if (!self::$instance) {
self::$instance = new Doctrine();
}
return self::$instance;
}
static public function connection()
{
return self::getInstance()->getConnection();
}
public function getConnection()
{
return $this->conn;
}
}
By using the above class, this simple statement will get a Doctrine connection in both Symfony and our legacy code:
$conn = Doctrine::connection()
Now we can the same libraries on both Symfony and our legacy code. Yes, I know it's a hack, but it is easier to understand and maintain than the 10+ files and dozens of lines of code required to create even a simple Symfony bundle (and the legacy system won't recognize a bundle). I've put this class in a private composer package that can be used by both legacy systems and Symfony. This will make it much easier to keep things running until we can migrate all the legacy stuff to Symfony. After that is done, then I can do things the right way.
Related
I get following error:
There is no extension able to load the configuration for
"upload_images" (in
"/var/www/vhosts/diabetigraph-dev/vendor/verzeilberg/upload-images/src/Resources/services.yaml").
Looked for namespace "upload_images", found "none"
This are my files:
services.yaml
services:
verzeilberg\UploadImagesBundle\Service\Rotate:
autowire: true
upload_images:
version: 100
Configuration
namespace verzeilberg\UploadImagesBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('upload_images');
$treeBuilder->getRootNode()
->children()
->integerNode('version')->end()
->end();
return $treeBuilder;
}
}
UploadImagesExtension
namespace verzeilberg\UploadImagesBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class UploadImagesExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources'));
$loader->load('services.yaml');
$config = $this->processConfiguration(new Configuration(), $configs);
$container->setParameter('version', $config['version']);
}
}
What am I doing wrong?
The basic answer is that your upload_images configuration needs to be moved to it's own application level config/packages file:
# app/config/packages/upload_images.yaml
upload_images:
version: 100
In your bundle, the Configuration object represents the bundle's configuration. There is no upload_images.yaml sort of file in your bundle. The object is quite powerful so you can add defaults and options and what not.
Your bundle's extension takes care of processing the final configuration and making information such as parameters available to the rest of the system:
class UploadImagesExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// $configs is actually an array of array representing the contents of upload_files.yaml
$config = $this->processConfiguration(new Configuration(), $configs);
$container->setParameter('upload_files.version', $config['version']);
// Loading your bundle's services.yaml file is a different process which just happens to be kicked off by the loader
// Thanks to the setParameter above, %upload_files.version% can be used by services
$loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources'));
$loader->load('services.yaml');
}
}
It can be confusing and for me at least I had to read the docs multiple times and do quite a bit of experimenting to understand the overall process.
The confusion is one of the reasons that Symfony evolved from multiple bundles per application to one application bundle to no application bundle at all.
I might also add the Symfony uses composer recipes to simplify installing a bundle. The recipe not only adds the bundle to config/bundles.php but copies any default configuration file to config/packages as well. Unfortunately, there are additional steps required for this copy to happen (see the docs) so it's easiest to do things the old-fashion way and just tell the developer to create the config file via the bundle's README file.
I am trying to implement a SimpleCache concrete instance in one of my service classes to also allow caching, however I am having some issues at wiring the dependencies.
config/services.yml
services:
Psr\SimpleCache\CacheInterface: '#Symfony\Component\Cache\Simple\FilesystemCache'
src/Services/Shouter/Sources/Twitter.php
<?php
namespace App\Services\Shouter\Sources;
use Psr\SimpleCache\CacheInterface;
class Twitter
{
/**
* Cache instance
* #var Psr\SimpleCache\CacheInterface
*/
protected $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
}
This is the error that I get:
You have requested a non-existent service "Symfony\Component\Cache\Simple\FilesystemCache".
Fixed by adding Symfony\Component\Cache\Simple\FilesystemCache: in the services.yaml.
I'm trying to create reusable logging bundle which can have custom formatter for log messages.
Formatter is set in the main app's config file like that:
custom_logger:
formatter: AppBundle\Services\MessageFormatter
Then in the LoggerBundle/DependencyInjection/CustomLoggerExtension.php
after i receive this configuration i'm trying to get logger service and set formatter
class CustomLoggerExtension extends ConfigurableExtension
{
public function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
$class = $mergedConfig['formatter'];
$obj = new $class;
$container->get('custom.logger')->setFormatter($obj);
but the problem is that logger uses request stack
services:
custom.logger:
class: LoggerBundle\Services\Logger
arguments: ['#request_stack']
and so when i try to get service in loadInternal function it seems that the request stack is not initialized yet and i receive error: You have requested a non-existent service "request_stack"
What is the correct way of doing this?
EDIT (2016-04-21 21:54:11)
That's weird. Even if i remove request stack and successfully set formatter in loadInternal, it's not in the service when i'm getting it from the controller in main app.
I must be doing something really wrong :)
Ok, I think I figured it out.
When load() or as in my case loadInternal() method is called, symfony passes there completely new container which is then merged into the main one.
So if I request a service and it is created there, it's not saved anywhere but in the temporary container and is eventually destroyed (only service definitions are passed to the main one).
Since services are loaded(created) on demand, it's not right to instantiate it on bundle load.
So what you want to do instead is to configure definitions
Here is how I got it working:
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\Config\FileLocator;
use LoggerBundle\Formatter\MessageFormatterInterface;
class LoggerBundleExtension extends ConfigurableExtension implements CompilerPassInterface
{
protected $customFormatter;
public function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
if (array_key_exists('formatter', $mergedConfig)) {
$this->customFormatter = $mergedConfig['formatter'];
}
}
public function process(ContainerBuilder $containerBuilder)
{
if ($this->customFormatter) {
$class = $this->customFormatter;
//if formatter is service id
$serviceUsed = $containerBuilder->has($this->customFormatter);
if ($serviceUsed) {
$class = $containerBuilder->getDefinition($this->customFormatter)->getClass();
}
if (!class_exists($class) || !is_a($class, MessageFormatterInterface::class, true)) {
throw new \ErrorException('Invalid logger formatter');
}
if ($serviceUsed) {
$containerBuilder
->getDefinition('custom.logger')
->addMethodCall('setFormatter', array(new Reference($this->customFormatter)));
} else {
$containerBuilder
->getDefinition('custom.logger')
->addMethodCall('setFormatter', array(new Definition($this->customFormatter)));
}
}
}
}
In loadInternal() I check if formatter is set in main app's config and save it.
But main thing is happening in process() function (you have to implement Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface for it to run).
From the docs.
As process() is called after all extensions are loaded, it allows you to edit service definitions of other extensions as well as retrieving information about service definitions.
So, what I am doing is:
checking if formatter option value is a class name or a service id(if that's the case, I retrieve class name from service definition)
validate class name (checking if it's a valid class and making sure it implements required interface)
adding definition to my logger service which tells container to call setFormatter with provided arguments when logger service is created(when it is first retrieved from the container)
Basically
$containerBuilder->getDefinition('custom.logger')->addMethodCall('setFormatter', array(new Reference('my_formatter')));
is the same as declaring in your config:
services:
custom.logger:
class: LoggerBundle\Services\Logger
arguments: ['#request_stack']
calls:
- [setFormatter, ['#my_formatter']]
I have a bundle which was working quite well for some time. However, I had to add some custom configuration params to it, so I wrote some lines in the bundle's config.yml, something like this:
# ...
acme_my_bundle:
special_params: ['param_1', 'param_2']
The configuration is defined in the bundle's Configuration class:
namespace ACME\MyBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
*/
class Configuration implements ConfigurationInterface {
/**
* {#inheritdoc}
*/
public function getConfigTreeBuilder() {
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('acme_my_bundle');
$rootNode
->children()
->arrayNode('special_params')
->end()
->end();
return $treeBuilder;
}
}
The bundle is properly registered in AppKernel.php:
public function registerBundles() {
$bundles = array(
// ...
new ACME\MyBundle(),
// ...
);
// ...
return $bundles;
}
However, when I try to use my app, I get an error:
There is no extension able to load the configuration for "acme_my_bundle" (in (path_to_bundle)/MyBundle/DependencyInjection/../Resources/config/config.yml). Looked for namespace "acme_my_bundle", found none
I looked it up, but most results found were unsatisfactory - I eliminated the problems that came up during searching:
improper configuration structure
bundle not registered in the app kernel
config root node name different than the one returned from ACMEMyBundleExtension::getAlias()
I tried debugging the cause of the exception being thrown and discovered that when the YAML file loader tries to validate my config file, the container has no extensions:
var_dump($container->getExtensions()); // prints empty array - array(0) { }
It causes the validation to fail and the none part of the message to be displayed - there a no available extensions.
I tried debugging $this->extensions in ContainerBuilder::hasExtension() and for some reason the list is complete when the method is launched for the vendor bundles, but is empty for my bundle. It looks like that something in my bundle is still defined or registered incorrectly.
I changed the names off classes, etc. not to expose company code, excuse me for that if it causes confusion.
EDIT: I didn't explicitly mention it, but the Extension class is defined and the exception occurs when it is loaded - just as I have written above:
when the YAML file loader tries to validate my config file
To be more clear, here is my Extension class:
namespace ACME\MyBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class ACMEMyBundleExtension extends Extension {
/**
* {#inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container) {
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
// The exception is thrown here
$loader->load('config.yml');
}
}
Check your configuration reader in ACME\MyBundle\DependencyInjection\Configuration for $rootNode = $treeBuilder->root('BUNDLE_CONFIG_KEY');.
BUNDLE_CONFIG_KEY should be:
valid (same in ACME\MyBundle\DependencyInjection\Configuration and your config.yml
unique for the app
Also please check you're defining bundle configuration in correct way - it should be added to app/config/*.yml (one of global config files). Maybe you have added acme_my_bundle config in other custom bundle config files?
You have missed bundle extension class (ACME\MyBundle\DependencyInjection\ACMEMyExtension) as explained here http://symfony.com/doc/current/cookbook/bundles/extension.html. Cookbook entry for bundle configuration is here. Key in config.yml should be named only as acme_my.
Creating the Configuration class alone is not enough. You need to register a dependency injection extension and use the Configuration class in there.
Read more about in the How to Create Friendly Configuration for a Bundle cookbook:
[The Configuration] class can now be used in your load() method to merge configurations and force validation (e.g. if an additional option was passed, an exception will be thrown)
namespace Acme\MyBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AcmeMyBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
// ...
}
}
Naming your extension class according to the convention will make it's automatically loaded. More on creating DIC extension classes in Creating an Extension Class. You can also enable the extension manually, see Manually registering an extension class.
Iam trying to implement service for encoding password but it seems it doesn't work cause I get "You have requested a non-existent service user_service " error.
Here is my code :
Vendor/BundleNameBundle/Resources/config/services.yml
services:
user_service:
class: Morescreens\VideomanagerBundle\Service\UserService
arguments: ['#security.encoder_factory']
app/config/config.yml
imports:
- { resource: "#morescreensVideomanagerBundle/Resources/config/services.yml" }
code for my service
class UserService {
/**
* #var EncoderFactoryInterface
*/
private $encoderFactory;
public function __construct(EncoderFactoryInterface $encoderFactory)
{
$this->encoderFactory=$encoderFactory;
}
public function setEncodedPassword(User $user)
{
$encoder=$this->encoderFactory->getEncoder($user);
$password=$encoder->encodePassword($user->getPassword(),$user->getSalt());
$user->setPassword($password);
}
}
Inside mine controller:
$user=new User();
$user->setPassword('password');
$this->get('user_service')->setEncodedPassword($user);
EDIT:
I manually deleted cache folder and my service started to work.
Try looking at your src/{VendorName}/{BundleName}Bundle/DependencyInjection/{VendorName}{BundleName}Extension.php
This file should load your service definitions like this:
<?php
namespace Vendor\{BundleName}Bundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class Vendor{BundleName}Extension extends Extension
{
/**
* {#inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
//Load our YAML resources
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
This will load the service definition from src/{VendorName}/{BundleName}Bundle/Resources/config/services.yml into the container. If that still doesn't work try doing php app/console cache:clear. For speed reasons, symfony will basically aggregate all your service (and other configuration files) into a cached file so it doesn't have to read those files every time. The files that actually cache the service definitions are located:
app/cache/{envName}/ (The cache directory can be configured to be located wherever you want, this is just the default).
The files that have the relevent info for container service definitions are:
app{EnvName}DebugProjectContainer.php
app{EnvName}DebugProjectContainer.php.meta
app{EnvName}DebugProjectContainer.xml