* @author William DURAND */ abstract class AbstractCommand extends ContainerAwareCommand { /** * Additional Phing args to add in specialized commands. * @var array */ protected $additionalPhingArgs = array(); /** * Temporary XML schemas used on command execution. * @var array */ protected $tempSchemas = array(); /** * @var string */ protected $cacheDir = null; /** * The Phing output. * @string */ protected $buffer = null; /** * @var Symfony\Component\HttpKernel\Bundle\BundleInterface */ protected $bundle = null; /** * @var Boolean */ private $alreadyWroteConnection = false; /** * * @var InputInterface */ protected $input; /** * Return the package for a given bundle. * * @param Bundle $bundle * @param string $baseDirectory The base directory to exclude from prefix. * * @return string */ protected function getPackage(Bundle $bundle, $namespace = '', $baseDirectory = '') { $path = explode(DIRECTORY_SEPARATOR, realpath($bundle->getPath())); $bundle_namespace = explode('\\', $bundle->getNamespace()); $diff = array_diff($bundle_namespace, $path); if (empty($diff)) { // PSR-0 $length = count($bundle_namespace) * (-1); $package = implode( DIRECTORY_SEPARATOR, array_merge( array_slice($path, 0, $length), explode('\\', $namespace) ) ); } else { // PSR-4 $ns = explode('\\', $namespace); $diff = array_diff($ns, $bundle_namespace); $package = implode( DIRECTORY_SEPARATOR, array_merge($path, $diff) ); } $package = ltrim(str_replace($baseDirectory, '', $package), DIRECTORY_SEPARATOR); if (!empty($package)) { $package = str_replace(DIRECTORY_SEPARATOR, '.', $package); } return $package; } /** * {@inheritdoc} */ protected function initialize(InputInterface $input, OutputInterface $output) { parent::initialize($input, $output); if ($input->getOption('verbose')) { $this->additionalPhingArgs[] = 'verbose'; } $this->input = $input; $this->checkConfiguration(); if ($input->hasArgument('bundle') && $input->getArgument('bundle')) { $bundleName = $input->getArgument('bundle'); if (0 === strpos($bundleName, '@')) { $bundleName = substr($bundleName, 1); } $this->bundle = $this->getContainer()->get('kernel')->getBundle($bundleName); } } /** * Call a Phing task. * * @param string $taskName A Propel task name. * @param array $properties An array of properties to pass to Phing. */ protected function callPhing($taskName, $properties = array()) { $kernel = $this->getApplication()->getKernel(); if (isset($properties['propel.schema.dir'])) { $this->cacheDir = $properties['propel.schema.dir']; } else { $this->cacheDir = $kernel->getCacheDir().'/propel'; $filesystem = new Filesystem(); $filesystem->remove($this->cacheDir); $filesystem->mkdir($this->cacheDir); } $this->copySchemas($kernel, $this->cacheDir); // build.properties $this->createBuildPropertiesFile($kernel, $this->cacheDir.'/build.properties'); // buildtime-conf.xml $this->createBuildTimeFile($this->cacheDir.'/buildtime-conf.xml'); // Verbosity $bufferPhingOutput = $this->getContainer()->getParameter('kernel.debug'); // Phing arguments $args = $this->getPhingArguments($kernel, $this->cacheDir, $properties); // Add any arbitrary arguments last foreach ($this->additionalPhingArgs as $arg) { if (in_array($arg, array('verbose', 'debug'))) { $bufferPhingOutput = false; } $args[] = '-'.$arg; } $args[] = $taskName; // Enable output buffering Phing::setOutputStream(new \OutputStream(fopen('php://output', 'w'))); Phing::setErrorStream(new \OutputStream(fopen('php://output', 'w'))); Phing::startup(); Phing::setProperty('phing.home', getenv('PHING_HOME')); ob_start(); $phing = new Phing(); $returnStatus = true; // optimistic way try { $phing->execute($args); $phing->runBuild(); $this->buffer = ob_get_contents(); // Guess errors if (strstr($this->buffer, 'failed. Aborting.') || strstr($this->buffer, 'Failed to execute') || strstr($this->buffer, 'failed for the following reason:')) { $returnStatus = false; } } catch (\Exception $e) { $returnStatus = false; } if ($bufferPhingOutput) { ob_end_clean(); } else { ob_end_flush(); } return $returnStatus; } /** * @param KernelInterface $kernel The application kernel. */ protected function copySchemas(KernelInterface $kernel, $cacheDir) { $filesystem = new Filesystem(); if (!is_dir($cacheDir)) { $filesystem->mkdir($cacheDir); } $base = ltrim(realpath($kernel->getRootDir().'/..'), DIRECTORY_SEPARATOR); $finalSchemas = $this->getFinalSchemas($kernel, $this->bundle); foreach ($finalSchemas as $schema) { list($bundle, $finalSchema) = $schema; $tempSchema = $bundle->getName().'-'.$finalSchema->getBaseName(); $this->tempSchemas[$tempSchema] = array( 'bundle' => $bundle->getName(), 'basename' => $finalSchema->getBaseName(), 'path' => $finalSchema->getPathname(), ); $file = $cacheDir.DIRECTORY_SEPARATOR.$tempSchema; $filesystem->copy((string) $finalSchema, $file, true); // the package needs to be set absolute // besides, the automated namespace to package conversion has // not taken place yet so it needs to be done manually $database = simplexml_load_file($file); if (isset($database['package'])) { // Do not use the prefix! // This is used to override the package resulting from namespace conversion. $package = $database['package']; } elseif (isset($database['namespace'])) { $package = $this->getPackage($bundle, $database['namespace'], $base); } else { throw new \RuntimeException( sprintf('%s : Please define a `package` attribute or a `namespace` attribute for schema `%s`', $bundle->getName(), $finalSchema->getBaseName()) ); } $database['package'] = $package; if ($this->input && $this->input->hasOption('connection') && $this->input->getOption('connection') && $database['name'] != $this->input->getOption('connection')) { //we skip this schema because the connection name doesn't match the input value unset($this->tempSchemas[$tempSchema]); $filesystem->remove($file); continue; } foreach ($database->table as $table) { if (isset($table['package'])) { $table['package'] = $table['package']; } else { $table['package'] = $package; } } file_put_contents($file, $database->asXML()); } } /** * Return a list of final schema files that will be processed. * * @param \Symfony\Component\HttpKernel\KernelInterface $kernel * * @return array */ protected function getFinalSchemas(KernelInterface $kernel, BundleInterface $bundle = null) { if (null !== $bundle) { return $this->getSchemasFromBundle($bundle); } $finalSchemas = array(); foreach ($kernel->getBundles() as $bundle) { $finalSchemas = array_merge($finalSchemas, $this->getSchemasFromBundle($bundle)); } return $finalSchemas; } /** * @param \Symfony\Component\HttpKernel\Bundle\BundleInterface $bundle * * @return array */ protected function getSchemasFromBundle(BundleInterface $bundle) { $finalSchemas = array(); if (is_dir($dir = $bundle->getPath().'/Resources/config')) { $finder = new Finder(); $schemas = $finder->files()->name('*schema.xml')->followLinks()->in($dir); if (iterator_count($schemas)) { foreach ($schemas as $schema) { $logicalName = $this->transformToLogicalName($schema, $bundle); $finalSchema = new \SplFileInfo($this->getFileLocator()->locate($logicalName)); $finalSchemas[(string) $finalSchema] = array($bundle, $finalSchema); } } } return $finalSchemas; } /** * @param \SplFileInfo $file * @return string */ protected function getRelativeFileName(\SplFileInfo $file) { return substr(str_replace(realpath($this->getContainer()->getParameter('kernel.root_dir') . '/../'), '', $file), 1); } /** * Create a 'build.properties' file. * * @param KernelInterface $kernel The application kernel. * @param string $file Should be 'build.properties'. */ protected function createBuildPropertiesFile(KernelInterface $kernel, $file) { $filesystem = new Filesystem(); $buildPropertiesFile = $kernel->getRootDir().'/config/propel.ini'; if (file_exists($buildPropertiesFile)) { $filesystem->copy($buildPropertiesFile, $file); } else { $filesystem->touch($file); } } /** * Create an XML file which represents propel.configuration * * @param string $file Should be 'buildtime-conf.xml'. */ protected function createBuildTimeFile($file) { $container = $this->getContainer(); if (!$container->has('propel.configuration')) { throw new \InvalidArgumentException('Could not find Propel configuration.'); } $xml = strtr(<< EOT , array('%default_connection%' => $container->getParameter('propel.dbal.default_connection'))); $propelConfiguration = $container->get('propel.configuration'); foreach ($propelConfiguration['datasources'] as $name => $datasource) { if (is_scalar($datasource)) { continue; } $xml .= strtr(<< %adapter% %dsn% %username% %password% EOT , array( '%name%' => $name, '%adapter%' => $datasource['adapter'], '%dsn%' => $datasource['connection']['dsn'], '%username%' => $datasource['connection']['user'], '%password%' => isset($datasource['connection']['password']) ? $datasource['connection']['password'] : '', )); } $xml .= << EOT; file_put_contents($file, $xml); } /** * Returns an array of properties as key/value pairs from an input file. * * @param string $file A file properties. * @return array An array of properties as key/value pairs. */ protected function getProperties($file) { $properties = array(); if (false === $lines = @file($file)) { throw new \Exception(sprintf('Unable to parse contents of "%s".', $file)); } foreach ($lines as $line) { $line = trim($line); if ('' == $line || in_array($line[0], array('#', ';'))) { continue; } $pos = strpos($line, '='); $property = trim(substr($line, 0, $pos)); $value = trim(substr($line, $pos + 1)); if ("true" === $value) { $value = true; } elseif ("false" === $value) { $value = false; } $properties[$property] = $value; } return $properties; } /** * Return the current Propel cache directory. * @return string The current Propel cache directory. */ protected function getCacheDir() { return $this->cacheDir; } /** * @return \Symfony\Component\Config\FileLocatorInterface */ protected function getFileLocator() { return $this->getContainer()->get('file_locator'); } /** * Get connection by checking the input option named 'connection'. * Returns the default connection if no option specified or an exception * if the specified connection doesn't exist. * * @param InputInterface $input * @param OutputInterface $output * @throw \InvalidArgumentException If the connection does not exist. * @return array */ protected function getConnection(InputInterface $input, OutputInterface $output) { $propelConfiguration = $this->getContainer()->get('propel.configuration'); $name = $input->getOption('connection') ?: $this->getContainer()->getParameter('propel.dbal.default_connection'); if (isset($propelConfiguration['datasources'][$name])) { $defaultConfig = $propelConfiguration['datasources'][$name]; } else { throw new \InvalidArgumentException(sprintf('Connection named %s doesn\'t exist', $name)); } if (false === $this->alreadyWroteConnection) { $output->writeln(sprintf('Use connection named %s in %s environment.', $name, $this->getApplication()->getKernel()->getEnvironment()) ); $this->alreadyWroteConnection = true; } // prevent errors if (!isset($defaultConfig['connection']['password'])) { $defaultConfig['connection']['password'] = null; } return array($name, $defaultConfig); } /** * Extract the database name from a given DSN * * @param string $dsn A DSN * @return string The database name extracted from the given DSN */ protected function parseDbName($dsn) { preg_match('#dbname=([a-zA-Z0-9\_]+)#', $dsn, $matches); if (isset($matches[1])) { return $matches[1]; } // e.g. SQLite return null; } /** * Check the PropelConfiguration object. */ protected function checkConfiguration() { $parameters = $this->getContainer()->get('propel.configuration')->getParameters(); if (!isset($parameters['datasources']) || 0 === count($parameters['datasources'])) { throw new \RuntimeException('Propel should be configured (no database configuration found).'); } } /** * Write Propel output as summary based on a Regexp. * * @param OutputInterface $output The output object. * @param string $taskname A task name */ protected function writeSummary(OutputInterface $output, $taskname) { foreach (explode("\n", $this->buffer) as $line) { if (false !== strpos($line, '[' . $taskname . ']')) { $arr = preg_split('#\[' . $taskname . '\] #', $line); $info = $arr[1]; if ('"' === $info[0]) { $info = sprintf('%s', $info); } $output->writeln($info); } } } /** * Comes from the SensioGeneratorBundle. * @see https://github.com/sensio/SensioGeneratorBundle/blob/master/Command/Helper/DialogHelper.php#L52 * * @param OutputInterface $output The output. * @param string $text A text message. * @param string $style A style to apply on the section. */ protected function writeSection(OutputInterface $output, $text, $style = 'bg=blue;fg=white') { $output->writeln(array( '', $this->getHelperSet()->get('formatter')->formatBlock($text, $style, true), '', )); } /** * Renders an error message if a task has failed. * * @param OutputInterface $output The output. * @param string $taskName A task name. * @param Boolean $more Whether to add a 'more details' message or not. */ protected function writeTaskError($output, $taskName, $more = true) { $moreText = $more ? ' To get more details, run the command with the "--verbose" option.' : ''; return $this->writeSection($output, array( '[Propel] Error', '', 'An error has occured during the "' . $taskName . '" task process.' . $moreText ), 'fg=white;bg=red'); } /** * @param OutputInterface $output The output. * @param string $filename The filename. */ protected function writeNewFile(OutputInterface $output, $filename) { $output->writeln('>> File+ ' . $filename); } /** * @param OutputInterface $output The output. * @param string $directory The directory. */ protected function writeNewDirectory(OutputInterface $output, $directory) { $output->writeln('>> Dir+ ' . $directory); } /** * Ask confirmation from the user. * * @param OutputInterface $output The output. * @param string $question A given question. * @param string $default A default response. */ protected function askConfirmation(OutputInterface $output, $question, $default = null) { return $this->getHelperSet()->get('dialog')->askConfirmation($output, $question, $default); } /** * @param \SplFileInfo $schema * @param BundleInterface $bundle * @return string */ protected function transformToLogicalName(\SplFileInfo $schema, BundleInterface $bundle) { $schemaPath = str_replace( $bundle->getPath(). DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR, '', $schema->getRealPath() ); return sprintf('@%s/Resources/config/%s', $bundle->getName(), $schemaPath); } /** * Compiles arguments/properties for the Phing process. * @return array */ private function getPhingArguments(KernelInterface $kernel, $workingDirectory, $properties) { $args = array(); // Default properties $properties = array_merge(array( 'propel.database' => 'mysql', 'project.dir' => $workingDirectory, 'propel.output.dir' => $kernel->getRootDir().'/propel', 'propel.php.dir' => $kernel->getRootDir().'/..', 'propel.packageObjectModel' => true, 'propel.useDateTimeClass' => true, 'propel.dateTimeClass' => 'DateTime', 'propel.defaultTimeFormat' => '', 'propel.defaultDateFormat' => '', 'propel.addClassLevelComment' => false, 'propel.defaultTimeStampFormat' => '', 'propel.builder.pluralizer.class' => 'builder.util.StandardEnglishPluralizer', ), $properties); // Adding user defined properties from the configuration $properties = array_merge( $properties, $this->getContainer()->get('propel.build_properties')->getProperties() ); foreach ($properties as $key => $value) { $args[] = "-D$key=$value"; } // Build file $args[] = '-f'; $args[] = realpath($this->getContainer()->getParameter('propel.path').'/generator/build.xml'); return $args; } }