Symfony 2: Access updated configuration from inside a command - php

We are creating a command that relies on other commands to generate a new database and build out its schema. So far we have successfully gotten it to read the config.yml file, add our new connection information, and to write the file back. In the same command we are then trying to run the symfony commands to create the database and schema:update. This is where we are running into problems. We get the following error:
[InvalidArgumentException] Doctrine ORM Manager named "mynewdatabase"
does not exist.
If we run the command a second time there is no error because the updated configuration file is loaded fresh into the application. If we manually run the doctrine commands after writing to the config.yml file it also works without error.
We are thinking that at the point in our command where we're running the database create and update commands, it's still using the current kernel's version of the config.yml/database.yml that are stored in memory. We have tried a number of different ways to reinitialize the application/kernel configuration (calling shutdown(), boot(), etc) without luck. Here's the code:
namespace Test\MyBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
class GeneratorCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('generate')
->setDescription('Create a new database.')
->addArgument('dbname', InputArgument::REQUIRED, 'The db name')
;
}
/*
example: php app/console generate mynewdatabase
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
//Without this, the doctrine commands will prematurely end execution
$this->getApplication()->setAutoExit(false);
//Open up app/config/config.yml
$yaml = Yaml::parse(file_get_contents($this->getContainer()->get('kernel')->getRootDir() .'/config/config.yml'));
//Take input dbname and use it to name the database
$db_name = $input->getArgument('dbname');
//Add that connection to app/config/config.yml
$yaml['doctrine']['dbal']['connections'][$site_name] = Array('driver' => '%database_driver%', 'host' => '%database_host%', 'port' => '%database_port%', 'dbname' => $site_name, 'user' => '%database_user%', 'password' => '%database_password%', 'charset' => 'UTF8');
$yaml['doctrine']['orm']['entity_managers'][$site_name] = Array('connection' => $site_name, 'mappings' => Array('MyCustomerBundle' => null));
//Now put it back
$new_yaml = Yaml::dump($yaml, 5);
file_put_contents($this->getContainer()->get('kernel')->getRootDir() .'/config/config.yml', $new_yaml);
/* http://symfony.com/doc/current/components/console/introduction.html#calling-an-existing-command */
//Set up our db create script arguments
$args = array(
'command' => 'doctrine:database:create',
'--connection' => $site_name,
);
$db_create_input = new ArrayInput($args);
//Run the symfony database create arguments
$this->getApplication()->run($db_create_input, $output);
//Set up our schema update script arguments
$args = array(
'command' => 'doctrine:schema:update',
'--em' => $site_name,
'--force' => true
);
$update_schema_input = new ArrayInput($args);
//Run the symfony database create command
$this->getApplication()->run($update_schema_input, $output);
}
}

The reason this doesn't work is because the DIC goes through a compilation process and is then written to a PHP file which is then included into the current running process. Which you can see here:
https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/Kernel.php#L562
If you change the service definitions and then try to "reboot" the kernel to compile these changes it won't include the compiled file a second time (require_once) and it will just create another instance of the already included DIC class with the old compiled service definitions.
The simplest way I can think of to get around this is to create an empty Kernel class that simply extends your AppKernel. Like so:
<?php
namespace Test\MyBundle\Command;
class FakeKernel extends \AppKernel
{
}
Then in your command, you can boot up this kernel after you've saved the new service definitions and it will re-compile a new DIC class using the "FakeKernel" name as part of the file name which means it will be included. like so:
$kernel = new \Test\MyBundle\Command\FakeKernel($input->getOption('env'), true);
$application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel);
Then you run your sub-commands against this new application which will be running with the new DIC:
$application->run($db_create_input, $output);
disclaimer: this feels very hacky. I'm open to hearing other solutions/workarounds.

Related

Symfony 4.4 Upgrade - DoctrineUpdateCommand no longer called from console command

I am upgrading our Symfony application from version 3.4 to version 4.4, which includes an upgrade of Doctrine from 1.12 to 2.3. I had previously written a class that modified the results to the doctrine:schema:update command, which worked great, but appears not to be working now. The class is below.
To modify doctrine:schema:update, I created a class called DoctrineUpdateCommand, which extended \Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand, and placed it in the Command folder of the bundle. This was all that was needed. I referenced this answer to figure out how to do it: How to set up entity (doctrine) for database view in Symfony 2.
We need to override the doctrine:schema:update command because one of our entities refers to a MySQL view instead of a MySQL table. Further, the entity is referenced as both a stand alone entity, and as a many-to-many join. The override class caused the entity and join to be ignored. It also added a sql statement to create the view.
After the upgrade, if I run php console doctrine:schema:update --dump-sql, I get these results:
19:08:16 CRITICAL [console] Error thrown while running command "doctrine:schema:update --dump-sql". Message: "The table with name 'nest_qa.assignedrole_privilegerole' already exists." ["exception" => Doctrine\DBAL\Schema\SchemaException^ { …},"command" => "doctrine:schema:update --dump-sql","message" => "The table with name 'nest_qa.assignedrole_privilegerole' already exists."]
In SchemaException.php line 112:
The table with name 'nest_qa.assignedrole_privilegerole' already exists.
I am fairly certain that the extension of the doctrine command is no longer being called, and it's using the default command instead, but I can't figure out how to change that. Any help would be appreciated.
Here is the original class:
<?php
namespace ApiBundle\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Doctrine\ORM\Tools\SchemaTool;
class DoctrineUpdateCommand extends \Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand{
protected $ignoredEntities = array(
'ApiBundle\Entity\AssignedrolePrivilegerole',
);
protected $ignoreAssociationMappings = array(
'ApiBundle\Entity\Privilegerole' => 'assignedroles',
'ApiBundle\Entity\Assignedrole' => 'privilegeroles',
);
protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui) {
/** #var $metadata \Doctrine\ORM\Mapping\ClassMetadata */
$newMetadatas = array();
foreach ($metadatas as $metadata) {
if (array_key_exists($metadata->getName(), $this->ignoreAssociationMappings)) {
if(array_key_exists($this->ignoreAssociationMappings[$metadata->getName()], $metadata->getAssociationMappings())){
unset($metadata->associationMappings[$this->ignoreAssociationMappings[$metadata->getName()]]);
}
}
//If metadata element is not in the ignore array, add it to the new metadata array
if (!in_array($metadata->getName(), $this->ignoredEntities)){
array_push($newMetadatas, $metadata);
}
}
parent::executeSchemaCommand($input, $output, $schemaTool, $newMetadatas, $ui);
$output->writeln("------Create view for assignedrole_privilegerole");
$output->writeln("CREATE VIEW `assignedrole_privilegerole` AS select `Assignedrole`.`id` AS `assignedrole_id`,`Privilegerole`.`id` AS `privilegerole_id` from (`Assignedrole` join `Privilegerole`) where ((`Assignedrole`.`role_id` = `Privilegerole`.`role_id`) and ((`Assignedrole`.`unit_id` = `Privilegerole`.`unit_id`) or `Privilegerole`.`unit_id` in (select `Unittree`.`ancestor_id` from `Unittree` where (`Unittree`.`descendant_id` = `Assignedrole`.`unit_id`)))) ;");
}
}
Console commands in symfony <4.x were registered by scanning Command folder inside a bundle. Since bundles are obsolete in symfony 4+, you have to define commands in your services definition by tagging the command class, or use DI autoconfiguration.
Option 1: explicitly add console.command tag to the service definition:
services:
ApiBundle\Command\DoctrineUpdateCommand:
tags:
- { name: twig.extension }
Option 2: use DI autoconfiguration:
services:
ApiBundle\Command\DoctrineUpdateCommand:
autoconfigure: true
After your class is registered as a console command, it must override the default one.
See symfony docs for more: Console Command

Codeigniter 3 - How to use Forge Class to create a database schema?

I'm using CodeIgniter 3.1.0 to develop an app. In order to improve installation, I've written an Install_Controller, and an Install_Model.
I want to use Database Forge class to manage the database schema, but it's not very clear in user guide and nothing on Google helps.
I need to use $this->dbforge->create_database, because the user knows nothing about database, so all he will do is MySQL "Next, next, install" and then run a batch file that run PHP as web server, so from Chrome he can use URL to install the app.
User guide says: In order to initialize the Forge class, your database driver must already be running, since the forge class relies on it.
So I have setup the config/database.php with user, pwd, dbname and so on... Even because I need it to use in app.
When I try to load the URL to install the app, give me the error: Message: mysqli::real_connect(): (HY000/1049): Unknown database 'test'
So, how can I use Forge Class to create database schema, if I need to have it first?
Some code...
$db['default'] = array(
'dsn' => '',
'hostname' => 'localhost',
'username' => 'root',
'password' => 'root',
'database' => 'test',
'dbdriver' => 'mysqli'
);
$autoload['libraries'] = array('database');
class Install extends CI_Controller
{
public function __construct()
{
parent::__construct();
}
public function index()
{
$this->load->model('install_model');
$this->install_model->createDabase();
}
}
class Install_model extends CI_Model {
function __construct() {
parent::__construct();
}
function createDabase() {
$this->load->dbforge();
if ($this->dbforge->create_database('test'))
echo 'Database created!';
else
echo 'Database error!';
}
}
After try and get a lot of opinions, the only way is as I commented:
Remove database from autoloader;
Use multiple databases, the default one is the same, but create a second one having empty database name;
Use the second database with Forge Class, instead of the default, only for create database and tables;
Change manually for the the default database and use it after that.

Calling console command security:check from controller action produces Lock file not found response

(Symfony3)
I'm toying with the idea of setting up some simple cron tasks to generate security reports for our project managers so that they can schedule upgrade time for developers (vs. me forgetting to run them manually).
As a very basic check, I'll simply run...
php bin/console security:check
...to see what composer has to say about vulnerabilities. Ultimately I'd like to roll this output into an email or post it to a slack channel or basecamp job when the cron is run.
Problem
When I run the command from via terminal it works great. Running the command inside a controller always returns the response Lock file does not exist. I'm assuming this in reference to the composer.lock file at the root of the project. I can confirm that this file does in fact exist.
Following is the controller I'm currently using, which is adapted from this:
http://symfony.com/doc/current/console/command_in_controller.html
<?php
namespace Treetop1500\SecurityReportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class DefaultController extends Controller
{
public function indexAction($key)
{
if ($key != $this->getParameter('easy_cron_key')) {
throw new UnauthorizedHttpException("You are not authorized to access this page.");
}
$kernel = $this->get('kernel');
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput(array(
'command' => 'security:check'
));
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
// return new Response(""), if you used NullOutput()
return new Response($content);
}
}
$content always has the value "Lock file does not exist."
I realize there are probably better tools and ways to do this, however I would really like to understand why this is the generated response from in this controller action. Thank you for taking a look!
Pass absolute path to composer.lock file just like that:
php bin/console security:check /path/to/another/composer.lock
So in your example, that's would be:
$input = new ArrayInput([
'command' => 'security:check',
'lockfile' => '/path/to/another/composer.lock'
]);
Read more: SecurityCheckerCommand from SensioLabs. Optional argument is lockfile, which is checked by SecurityChecker. On line 46, they are looking for composer.lock file (default argument) and throw an exception, when they not found.
P.S. Earlier, I type the wrong parameters to array. I checked in Symfony documentation (How to Call Other Commands) and fixed the answer.
The solution to this is to pass the lockfile argument to the ArrayInput object like this:
$lockfile = $this->get('kernel')->getRootDir()."/../composer.lock";
$input = new ArrayInput(array('command'=>'security:check','lockfile'=>$lockfile));

Proper way to inject dynamic configuration into configuration array

I'm wondering what is the best way to inject dynamic configuration(retrieved from db for instance) into configuration array in Zend Framework 2? In Module.php I have:
public function onBootstrap(MvcEvent $e) {
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$eventManager->attach('route', array($this, 'mergeDynamicConfig'));
}
public function mergeDynamicConfig(EventInterface $e) {
$application = $e->getApplication();
$sm = $application->getServiceManager();
$configurationTable = $sm->get('DynamicConfiguration\Model\Table\ConfigurationTable');
$dynamicConfig = $configurationTable->fetchAllConfig();
//Configuration array from db
//Array
//(
// [config] => 'Test1',
// [config2] => 'Test2',
// [config3] => 'Test3',
//)
//What to do here?
//I want to use the configurations above like $sm->get('Config')['dynamic_config']['config3'];
}
There is a section in the documentation that explains how to manipulate the merged configuration using the specific event ModuleEvent::EVENT_MERGE_CONFIG
Zend\ModuleManager\Listener\ConfigListener triggers a special event, Zend\ModuleManager\ModuleEvent::EVENT_MERGE_CONFIG, after merging all configuration, but prior to it being passed to the ServiceManager. By listening to this event, you can inspect the merged configuration and manipulate it.
The problem with this is that the service manager is not available at this point as the listener's event is one of the first events triggered by the module manager at priority 1000).
This means that you cannot execute your query and merge the config prior to the configuration being passed to the service manager, you would need to do so after.
Perhaps I have misunderstood your requirements, however I would approach this differently.
You could replace any calls where you need config $serviceManager->get('config') with $serviceManager->get('MyApplicationConfig'); which would be you own configuration service that uses the merged application config and then adds to it.
For example, you could register this configuration service in module.config.php.
return [
'service_manager' => [
'factories' => [
'MyApplicationConfig' => 'MyApplicationConfig\Factory\MyApplicationConfigFactory',
]
],
];
And create a factory to do the loading of merged module configuration, making any database calls or caching etc.
class MyApplicationConfigFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $sm)
{
$config = $sm->get('config');
$dbConfig = $this->getDatabaseConfigAsArray($sm);
return array_replace_recursive($config, $dbConfig);
}
protected function getDatabaseConfigAsArray(ServiceLocatorInterface $sm)
{}
}
You also have the added benefit that the service is lazy loaded.
I would not use this approuch, for a few reasons.
Putting SQL queries in your Module.php means that they will get executed on EVERY request for every user thus making your application slow, very slow.
If your database is compromised all the config will be stolen as well.
Solution would be to move all the config in your config/autoload/my_custom_config.local.php via array with keys. From there you can always load it without making a single database request. It will be way faster and secure, because the file will be outside your root folder and hacking a server is always alot harder than hacking a database.
If you still want to allow users to eit the options you can simply include the file in an action and show it with a foreach for example. To save the information you can do this:
file_put_contents("my_custom_config.local.php", '<?php return ' . var_export($config, true).';');
One other plus is that if you load your config the way discribe above you can also retrive the config like you want via $sm->get('Config')['dynamic_config']['config3']

How to use Models in a Laravel Queue

I'm trying to import a mailing list from CSV to my DATABASE. I have two models in my Laravel which is responsible for doing this: Target and Mailing (one Target has many Mailings)
I'm using Queue system with Beanstalkd. I'm using:
Queue::push('ImportCSV', array(
'file' => $file->getClientOriginalName(),
'target' => $name
));
To push my jobs and then I have the ImportCSV job class:
class ImportCSV
{
public function fire($job, $data)
{
Log::info("Starting to add {$data['target']} to database");
$target = new Target();
$target->name = $data['target'];
$target->save();
$reader = new \EasyCSV\Reader($data['file']);
// There must be a Email field in CSV file
/*if(!in_array('Email', $reader->getHeaders() ))
throw new Exception("Email field not found", 1);*/
while ($row = $reader->getRow())
{
$mailing = new Mailing();
$mailing->target()->associate($target);
$mailing->email = $row['Email'];
$mailing->save();
}
Log::info("Mailing list {$target->name} added to database");
$job->delete();
}
}
All the code seems to be working since I get these messages in my Log file
[2013-09-10 21:03:25] log.INFO: Starting to add TEst to database [] []
[2013-09-10 21:03:25] log.INFO: Mailing list TEst added to database [] []
But no records are added to my database. How should I use models inside a job? I already tested it in a Controller for example and everything works fine
Since you don't see other errors, I'm thinking this is an environment issue.
First - environments
Make sure your call to php artisan queue:listen (or queue:work, if applicable) is using the correct environment so the correct database is getting used:
$ php artisan queue:listen --env=YOUR_ENV
Here's a post on setting up queues in Laravel 4 which might be helpful for more information.
Second - namespaces
As you (apparently?) aren't seeing any PHP errors, this is less likely, but another idea:
If your class is namespaced, you may need to use the \ character to get your models, which are in the global namespace.
// From:
$mailing = new Mailing();
// To:
$mailing = new \Mailing();

Categories