diff --git a/DataFixtures/Dumper/YamlDataDumper.php b/DataFixtures/Dumper/YamlDataDumper.php index dca5686f..89acfd52 100644 --- a/DataFixtures/Dumper/YamlDataDumper.php +++ b/DataFixtures/Dumper/YamlDataDumper.php @@ -27,8 +27,7 @@ protected function transformArrayToData($data) $data, $inline = 3, $indent = 4, - $exceptionOnInvalidType = false, - $objectSupport = true + Yaml::DUMP_OBJECT ); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 3a73a4a5..bf2ed764 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -10,8 +10,10 @@ namespace Propel\Bundle\PropelBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\HttpKernel\Kernel; /** * This class contains the configuration information for the bundle @@ -38,12 +40,20 @@ public function __construct($debug) /** * Generates the configuration tree builder. * - * @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder + * @return TreeBuilder The tree builder */ public function getConfigTreeBuilder() { + if (Kernel::MAJOR_VERSION > 4 || Kernel::MAJOR_VERSION === 4 && Kernel::MINOR_VERSION >= 2) + { + $treeBuilder = new TreeBuilder('propel'); + $rootNode = $treeBuilder->getRootNode(); + } + else + { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('propel'); + } $this->addGeneralSection($rootNode); $this->addDbalSection($rootNode); @@ -100,6 +110,8 @@ private function addGeneralSection(ArrayNodeDefinition $node) */ private function addDbalSection(ArrayNodeDefinition $node) { + $dNormalizer = new DriverNormalizer(); + $dClosure = function($v) use ($dNormalizer) { return $dNormalizer->normalize($v); }; $node ->children() ->arrayNode('dbal') @@ -112,7 +124,7 @@ private function addDbalSection(ArrayNodeDefinition $node) ->scalarNode('driver') ->beforeNormalization() ->always() - ->then(function($v) { return str_replace('pdo_', '', $v); }) + ->then($dClosure) ->end() ->defaultValue('mysql') ->end() @@ -121,7 +133,7 @@ private function addDbalSection(ArrayNodeDefinition $node) ->scalarNode('dsn') ->beforeNormalization() ->always() - ->then(function($v) { return str_replace('pdo_', '', $v); }) + ->then($dClosure) ->end() ->defaultValue('') ->end() @@ -145,14 +157,14 @@ private function addDbalSection(ArrayNodeDefinition $node) ->children() ->arrayNode('settings') ->useAttributeAsKey('key') - ->prototype('array') + ->arrayPrototype() ->useAttributeAsKey('key') ->prototype('scalar')->end() ->end() ->end() ->end() ->fixXmlConfig('connection') - ->append($this->getDbalConnectionsNode()) + ->append($this->getDbalConnectionsNode($dNormalizer)) ->end() ; } @@ -171,22 +183,32 @@ private function addDbalSection(ArrayNodeDefinition $node) * attributes: {} * settings: {} * - * @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder + * @return ArrayNodeDefinition|NodeDefinition The tree builder */ - private function getDbalConnectionsNode() + private function getDbalConnectionsNode(DriverNormalizer $normalizer) { - $treeBuilder = new TreeBuilder(); - $node = $treeBuilder->root('connections'); + $closure = function($v) use ($normalizer) { return $normalizer->normalize($v); }; + + if (Kernel::MAJOR_VERSION > 4 || Kernel::MAJOR_VERSION === 4 && Kernel::MINOR_VERSION >= 2) + { + $treeBuilder = new TreeBuilder('connections'); + $node = $treeBuilder->getRootNode(); + } + else + { + $treeBuilder = new TreeBuilder(); + $node = $treeBuilder->root('connections'); + }; $node ->requiresAtLeastOneElement() ->useAttributeAsKey('name') - ->prototype('array') + ->arrayPrototype() ->children() ->scalarNode('driver') ->beforeNormalization() ->always() - ->then(function($v) { return str_replace('pdo_', '', $v); }) + ->then($closure) ->end() ->defaultValue('mysql') ->end() @@ -195,19 +217,19 @@ private function getDbalConnectionsNode() ->scalarNode('dsn') ->beforeNormalization() ->always() - ->then(function($v) { return str_replace('pdo_', '', $v); }) + ->then($closure) ->end() ->defaultValue('') ->end() ->scalarNode('classname')->defaultValue($this->debug ? 'DebugPDO' : 'PropelPDO')->end() ->arrayNode('slaves') ->useAttributeAsKey('name') - ->prototype('array') + ->arrayPrototype() ->children() ->scalarNode('driver') ->beforeNormalization() ->always() - ->then(function($v) { return str_replace('pdo_', '', $v); }) + ->then($closure) ->end() ->defaultValue('mysql') ->end() @@ -216,7 +238,7 @@ private function getDbalConnectionsNode() ->scalarNode('dsn') ->beforeNormalization() ->always() - ->then(function($v) { return str_replace('pdo_', '', $v); }) + ->then($closure) ->end() ->defaultValue('') ->end() diff --git a/DependencyInjection/DriverNormalizer.php b/DependencyInjection/DriverNormalizer.php new file mode 100644 index 00000000..b82747c0 --- /dev/null +++ b/DependencyInjection/DriverNormalizer.php @@ -0,0 +1,47 @@ +normalize($adapter); + } + + if ($dsn = $datasource['connection']['dsn'] ?? null) + { + $datasource['connection']['dsn'] = $this->normalize($dsn); + } + + return $datasource; + } + + /** + * Removes the 'pdo_' prefix found in some database driver strings. + * + * @param string $value + * + * @return string + */ + public function normalize($value) + { + return str_replace('pdo_', '', $value); + } +} \ No newline at end of file diff --git a/DependencyInjection/EnvResolver.php b/DependencyInjection/EnvResolver.php deleted file mode 100644 index 1045ef17..00000000 --- a/DependencyInjection/EnvResolver.php +++ /dev/null @@ -1,80 +0,0 @@ -container = $container; } - - /** - * Resolves the given configs by replacing any env_XXXXX_rand placeholder values or env(ENV_VAR) parameters with - * their real values. - * - * @param array $configs An array of configurations, typically from the Symfony Configuration component. - * - * @return array The array of configurations with the env vars resolved. - */ - public function resolve($configs): array - { - if (static::isPrefixed($configs)) - { - return $this->container->resolveEnvPlaceholders($configs, true); - } - - return $configs; - } - - /** - * Checks for the prefixed string in the given multi-level array and returns TRUE if any of the placeholders are - * found at the beginning of a value. - * - * @param array $configs An array of configurations, typically from the Symfony Configuration component. - * @param string[] $prefixes An array of prefixes to check for - * - * @return bool TRUE if any of the prefixes are found in any of the values - */ - public static function isPrefixed($configs, $prefixes = array('env_','env(')): bool - { - foreach ($configs as $value) - { - if (is_array($value)) - { - if (true === static::isPrefixed($value, $prefixes)) - { - return true; - } - } - elseif (is_string($value)) - { - foreach ($prefixes as $prefix) - { - if (0 === strpos($value, $prefix)) - { - return true; - } - } - } - } - - return false; - } -} diff --git a/DependencyInjection/PropelExtension.php b/DependencyInjection/PropelExtension.php index c736e313..d97c746d 100644 --- a/DependencyInjection/PropelExtension.php +++ b/DependencyInjection/PropelExtension.php @@ -29,11 +29,6 @@ class PropelExtension extends Extension */ public function load(array $configs, ContainerBuilder $container) { - // WORKAROUND for https://github.com/symfony/symfony/issues/27683 https://github.com/symfony/symfony/issues/40906 - // Note that this may require the clearing of the cache after a related ENV var is changed. - $configs = (new EnvResolver($container))->resolve($configs); - // END WORKAROUND - $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); @@ -57,13 +52,8 @@ public function load(array $configs, ContainerBuilder $container) throw new \InvalidArgumentException('PropelBundle expects a "phing_path" parameter that must contain the absolute path to the Phing vendor library. The "phing_path" parameter must be defined under the "propel" root node in your configuration.'); } - if (isset($config['logging']) && $config['logging']) { - $logging = $config['logging']; - } else { - $logging = false; - } - - $container->setParameter('propel.logging', $logging); + // Set Logging Parameters + $container->setParameter('propel.logging', $config['logging'] ?? false); if (!empty($config['schema_path'])) { @@ -92,7 +82,12 @@ public function load(array $configs, ContainerBuilder $container) } } - $container->getDefinition('propel.build_properties')->setArguments(array($buildProperties)); + // Store the build_properties configuration at compile time, rather than injecting into service for Properties + // class. This prevents issues with resolution of environment variables, which must be resolved at runtime. + // The synthetic service is then set in PropelBundle::boot(). See these issues for details of problem: + // https://github.com/symfony/symfony/issues/27683 + // https://github.com/symfony/symfony/issues/40906 + $container->setParameter('propel.build_properties', $buildProperties); if (!empty($config['dbal'])) { $this->dbalLoad($config['dbal'], $container); @@ -143,7 +138,12 @@ protected function dbalLoad(array $config, ContainerBuilder $container) $c['datasources']['default'] = $connectionName; } - $container->getDefinition('propel.configuration')->setArguments(array($c)); + // Store the dbal configuration at compile time, rather than creating a service for the configuration object. + // This prevents issues with resolution of environment variables, which must be resolved at runtime. The + // synthetic service is then set in PropelBundle::boot(). See these issues for details of problem: + // https://github.com/symfony/symfony/issues/27683 + // https://github.com/symfony/symfony/issues/40906 + $container->setParameter('propel.dbal', $c); } public function getConfiguration(array $config, ContainerBuilder $container) diff --git a/DependencyInjection/PropelServiceFactory.php b/DependencyInjection/PropelServiceFactory.php new file mode 100644 index 00000000..3c77b60e --- /dev/null +++ b/DependencyInjection/PropelServiceFactory.php @@ -0,0 +1,199 @@ + + * @package Propel\Bundle\PropelBundle\DependencyInjection + */ +class PropelServiceFactory +{ + const CONFIG = 'propel.configuration'; + const DATA_COLLECTOR = 'propel.data_collector'; + const BUILD_PROPERTIES = 'propel.build_properties'; + + /** + * Creates a PropelDataCollector object using the 'propel.data_collector' parameter and 'propel.logger' service, + * then injects the object into the container as the 'propel.build_properties' service, replacing the synthetic. + * + * If the container does not already have the 'propel.configuration' service, it will be created and injected into + * the container as well. + * + * @param ContainerInterface $container + * + * @return PropelDataCollector|object + */ + public function collector(ContainerInterface $container): PropelDataCollector + { + $name = static::DATA_COLLECTOR; + if (!$container->has($name)) + { + // Make sure that we have a propel.configuration service + $config = $this->config($container); + // Get the logger Service + $logger = $container->get('propel.logger'); + // Get the class & create object + $class = $this->getClass($container, $name); + $object = new $class($logger, $config); + + $container->set($name, $object); + } + + return $container->get($name); + } + + /** + * Creates a \PropelConfiguration object using the 'propel.configuration.class' and 'propel.dbal' parameters, + * then injects the object into the container as the 'propel.configuration' service, replacing the synthetic. + * + * @param ContainerInterface $container + * + * @return \PropelConfiguration|object + */ + public function config(ContainerInterface $container): \PropelConfiguration + { + $name = static::CONFIG; + if (!$container->has($name)) + { + $class = $this->getClass($container, $name) ?? \PropelConfiguration::class; + $dbal = $this->normalizeDbal($this->getArrayParameter($container, 'propel.dbal')); + $config = new $class($dbal); + + if ($this->isPropelLogging($container)) + { + $config->setParameter('debugpdo.logging.methods', array( + 'PropelPDO::exec', + 'PropelPDO::query', + 'PropelPDO::prepare', + 'DebugPDOStatement::execute', + ), false); + + $config->setParameter('debugpdo.logging.details', array( + 'time' => array('enabled' => true), + 'mem' => array('enabled' => true), + 'connection' => array('enabled' => true), + )); + } + + $container->set($name, $config); + } + + return $container->get($name); + } + + /** + * Creates a Properties object using the 'propel.build_properties' and 'propel.build_properties.class' parameters, + * then injects the object into the container as the 'propel.build_properties' service, replacing the synthetic. + * + * @param ContainerInterface $container + * + * @return Properties|object + */ + public function properties(ContainerInterface $container): Properties + { + $name = static::BUILD_PROPERTIES; + if (!$container->has($name)) + { + $class = $this->getClass($container, $name) ?? Properties::class; + $props = $this->getArrayParameter($container, $name); + $obj = new $class($props); + + $container->set($name, $obj); + } + + return $container->get($name); + } + + /** + * Evaluates if the 'logging' option in the propel configuration is set to TRUE. + * Requires that the 'propel.logging' parameter is previously injected into the container, + * typically in PropelExtension. + * + * @param ContainerInterface $container + * + * @return bool + */ + public function isPropelLogging(ContainerInterface $container) + { + return $container->hasParameter('propel.logging') && (bool)$container->getParameter('propel.logging'); + } + + /** + * Normalizes config settings containing the database driver. While this is also done in the configuration at + * compile time, some values may be environment value placeholders, which cannot be normalized. + * + * @param array $dbal + * + * @return array + */ + private function normalizeDbal($dbal) + { + $normalizer = new DriverNormalizer(); + $datasources = $dbal['datasources'] ?? array(); + + foreach($datasources as $name => $datasource) + { + $datasources[$name] = $normalizer->normalizeDatasource($datasource); + } + + $dbal['datasources'] = $datasources; + + return $dbal; + } + + /** + * Checks for and returns a parameter as an array. Returns an empty array if the parameter + * is not present in the container, or is not an array value. + * + * @param ContainerInterface $container + * @param string $name + * + * @return array + */ + private function getArrayParameter(ContainerInterface $container, string $name): array + { + if ($container->hasParameter($name)) + { + $props = $container->getParameter($name); + + if (is_array($props)) + { + return $props; + } + } + + return array(); + } + + /** + * Gets the fully qualified class name for the given prefix, as long as the parameter is set in the container. + * + * @param ContainerInterface $container The container, which should have the .class parameter. + * @param string $prefix The string to put before .class when retrieving the parameter. + * + * @return string|null + */ + private function getClass(ContainerInterface $container, string $prefix) + { + $name = $prefix . '.class'; + if ($container->hasParameter($name)) + { + $class = $container->getParameter($name); + if (is_string($class) && class_exists($class)) + { + return $class; + } + } + + return null; + } +} \ No newline at end of file diff --git a/PropelBundle.php b/PropelBundle.php index e151887e..12ec8545 100644 --- a/PropelBundle.php +++ b/PropelBundle.php @@ -9,6 +9,7 @@ */ namespace Propel\Bundle\PropelBundle; +use Propel\Bundle\PropelBundle\DependencyInjection\PropelServiceFactory; use Propel\Bundle\PropelBundle\DependencyInjection\Security\UserProvider\PropelFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -34,26 +35,28 @@ public function boot() get_include_path()); } + // Create the Service Factory + $Factory = new PropelServiceFactory(); + + // Set the Build Properties Service + $Factory->properties($this->container); + + // Initialize Propel if (!\Propel::isInit()) { - \Propel::setConfiguration($this->container->get('propel.configuration')); + // Create Configuration object at runtime to prevent issues with environment variables + // used in the configuration. If this is done at compile time, they aren't resolved. + // https://github.com/symfony/symfony/issues/27683 + // https://github.com/symfony/symfony/issues/40906 + $config = $Factory->config($this->container); + + // Set the configuration created above, which has also been injected into the container as a service. + \Propel::setConfiguration($config); - if ($this->container->getParameter('propel.logging')) { - $config = $this - ->container - ->get('propel.configuration') - ; - $config->setParameter('debugpdo.logging.methods', array( - 'PropelPDO::exec', - 'PropelPDO::query', - 'PropelPDO::prepare', - 'DebugPDOStatement::execute', - ), false); - $config->setParameter('debugpdo.logging.details', array( - 'time' => array('enabled' => true), - 'mem' => array('enabled' => true), - 'connection' => array('enabled' => true), - )); + // Initialize the propel.data_collector Service + $Factory->collector($this->container); + // The factory above sets the logging parameters in PropelConfiguration, if param 'propel.logging' is set. + if ($Factory->isPropelLogging($this->container)) { \Propel::setLogger($this->container->get('propel.logger')); } diff --git a/Resources/config/propel.xml b/Resources/config/propel.xml index f4739a98..da68efb0 100644 --- a/Resources/config/propel.xml +++ b/Resources/config/propel.xml @@ -17,9 +17,9 @@ - + - + @@ -27,10 +27,8 @@ - + - - diff --git a/composer.json b/composer.json index 471ed528..b6ad5397 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "autoload": { "psr-4": { "Propel\\Bundle\\PropelBundle\\": "" }, "files": ["autoload_aliases.php"], - "exclude-from-classmap": [ "Tests/","vendor/propel/propel1/runtime/lib","vendor/propel/propel1/generator/lib" ] + "exclude-from-classmap": [ "Tests/","vendor/palepurple/propel1/runtime/lib","vendor/palepurple/propel1/generator/lib" ] }, "version": "1.8.1", "require": {