From 98bc277a08af76e7f498b37b13dada76be727e8f Mon Sep 17 00:00:00 2001 From: Thomas Steur <thomas.steur@googlemail.com> Date: Thu, 3 Apr 2014 01:11:18 +0200 Subject: [PATCH] started to work on command to set number of available custom variables --- core/Db.php | 13 ++ core/Db/Schema/Mysql.php | 31 +-- .../Commands/SetNumberOfCustomVariables.php | 190 ++++++++++++++++++ plugins/CustomVariables/CustomVariables.php | 11 + plugins/CustomVariables/Model.php | 124 ++++++++++++ plugins/CustomVariables/tests/ModelTest.php | 113 +++++++++++ tests/PHPUnit/Integration/Core/DbTest.php | 32 +++ 7 files changed, 484 insertions(+), 30 deletions(-) create mode 100644 plugins/CustomVariables/Commands/SetNumberOfCustomVariables.php create mode 100644 plugins/CustomVariables/Model.php create mode 100644 plugins/CustomVariables/tests/ModelTest.php create mode 100644 tests/PHPUnit/Integration/Core/DbTest.php diff --git a/core/Db.php b/core/Db.php index f099d01409..3647a5878f 100644 --- a/core/Db.php +++ b/core/Db.php @@ -330,6 +330,19 @@ class Db return self::query("DROP TABLE " . implode(',', $tables)); } + /** + * Get columns information from table + * + * @param string|array $table The name of the table you want to get the columns definition for. + * @return \Zend_Db_Statement + */ + static public function getColumnNamesFromTable($table) + { + $columns = self::fetchAssoc("SHOW COLUMNS FROM " . $table); + + return array_keys($columns); + } + /** * Locks the supplied table or tables. * diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php index fa326a9dc4..34c3764f2f 100644 --- a/core/Db/Schema/Mysql.php +++ b/core/Db/Schema/Mysql.php @@ -192,16 +192,6 @@ class Mysql implements SchemaInterface location_city varchar(255) DEFAULT NULL, location_latitude float(10, 6) DEFAULT NULL, location_longitude float(10, 6) DEFAULT NULL, - custom_var_k1 VARCHAR(200) DEFAULT NULL, - custom_var_v1 VARCHAR(200) DEFAULT NULL, - custom_var_k2 VARCHAR(200) DEFAULT NULL, - custom_var_v2 VARCHAR(200) DEFAULT NULL, - custom_var_k3 VARCHAR(200) DEFAULT NULL, - custom_var_v3 VARCHAR(200) DEFAULT NULL, - custom_var_k4 VARCHAR(200) DEFAULT NULL, - custom_var_v4 VARCHAR(200) DEFAULT NULL, - custom_var_k5 VARCHAR(200) DEFAULT NULL, - custom_var_v5 VARCHAR(200) DEFAULT NULL, PRIMARY KEY(idvisit), INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), INDEX index_idsite_datetime (idsite, visit_last_action_time), @@ -264,16 +254,6 @@ class Mysql implements SchemaInterface revenue_shipping float default NULL, revenue_discount float default NULL, - custom_var_k1 VARCHAR(200) DEFAULT NULL, - custom_var_v1 VARCHAR(200) DEFAULT NULL, - custom_var_k2 VARCHAR(200) DEFAULT NULL, - custom_var_v2 VARCHAR(200) DEFAULT NULL, - custom_var_k3 VARCHAR(200) DEFAULT NULL, - custom_var_v3 VARCHAR(200) DEFAULT NULL, - custom_var_k4 VARCHAR(200) DEFAULT NULL, - custom_var_v4 VARCHAR(200) DEFAULT NULL, - custom_var_k5 VARCHAR(200) DEFAULT NULL, - custom_var_v5 VARCHAR(200) DEFAULT NULL, PRIMARY KEY (idvisit, idgoal, buster), UNIQUE KEY unique_idsite_idorder (idsite, idorder), INDEX index_idsite_datetime ( idsite, server_time ) @@ -293,16 +273,7 @@ class Mysql implements SchemaInterface idaction_event_category INTEGER(10) UNSIGNED DEFAULT NULL, idaction_event_action INTEGER(10) UNSIGNED DEFAULT NULL, time_spent_ref_action INTEGER(10) UNSIGNED NOT NULL, - custom_var_k1 VARCHAR(200) DEFAULT NULL, - custom_var_v1 VARCHAR(200) DEFAULT NULL, - custom_var_k2 VARCHAR(200) DEFAULT NULL, - custom_var_v2 VARCHAR(200) DEFAULT NULL, - custom_var_k3 VARCHAR(200) DEFAULT NULL, - custom_var_v3 VARCHAR(200) DEFAULT NULL, - custom_var_k4 VARCHAR(200) DEFAULT NULL, - custom_var_v4 VARCHAR(200) DEFAULT NULL, - custom_var_k5 VARCHAR(200) DEFAULT NULL, - custom_var_v5 VARCHAR(200) DEFAULT NULL, + custom_float FLOAT NULL DEFAULT NULL, PRIMARY KEY(idlink_va), INDEX index_idvisit(idvisit), diff --git a/plugins/CustomVariables/Commands/SetNumberOfCustomVariables.php b/plugins/CustomVariables/Commands/SetNumberOfCustomVariables.php new file mode 100644 index 0000000000..57fa3f174d --- /dev/null +++ b/plugins/CustomVariables/Commands/SetNumberOfCustomVariables.php @@ -0,0 +1,190 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\CustomVariables\Commands; + +use Piwik\Plugin\ConsoleCommand; +use Piwik\Plugins\CustomVariables\Model; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + */ +class SetNumberOfCustomVariables extends ConsoleCommand +{ + /** + * @var \Symfony\Component\Console\Helper\ProgressHelper + */ + private $progress; + + protected function configure() + { + $this->setName('customvariables:set-number-available-custom-variables'); + $this->setDescription('Change the number of available custom variables'); + $this->setHelp("Example: +./console customvariables:set-number-available-custom-variables 10 +=> 10 custom variables will be available in total +"); + $this->addArgument('maxCustomVars', InputArgument::REQUIRED, 'The number of available custom variables'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $numVarsToSet = $this->getNumVariablesToSet($input); + $numChangesToPerform = $this->getNumberOfChangesToPerform($numVarsToSet); + + if (0 === $numChangesToPerform) { + $this->writeSuccessMessage($output, array( + 'Your Piwik is already configured for ' . $numVarsToSet . ' custom variables.' + )); + return; + } + + foreach (Model::getScopes() as $scope) { + $this->printChanges($scope, $numVarsToSet, $output); + } + + if (!$this->confirmChange($output)) { + return; + } + + $output->writeln(''); + $output->writeln('Starting to apply changes'); + $output->writeln(''); + + $this->progress = $this->initProgress($numChangesToPerform, $output); + + foreach (Model::getScopes() as $scope) { + $this->performChange($scope, $numVarsToSet, $output); + } + + $this->progress->finish(); + + $this->writeSuccessMessage($output, array( + 'Your Piwik is now configured for ' . $numVarsToSet . ' custom variables.' + )); + } + + private function initProgress($numChangesToPerform, OutputInterface $output) + { + /** @var \Symfony\Component\Console\Helper\ProgressHelper $progress */ + $progress = $this->getHelperSet()->get('progress'); + $progress->start($output, $numChangesToPerform); + + return $progress; + } + + private function performChange($scope, $numVarsToSet, OutputInterface $output) + { + $model = new Model($scope); + $numCurrentVars = $model->getCurrentNumCustomVars(); + $numDifference = $this->getAbsoluteDifference($numCurrentVars, $numVarsToSet); + + if ($numVarsToSet > $numCurrentVars) { + $this->addCustomVariables($model, $numDifference, $output); + return; + } + + $this->removeCustomVariables($model, $numDifference, $output); + } + + private function getNumVariablesToSet(InputInterface $input) + { + $maxCustomVars = $input->getArgument('maxCustomVars'); + + if (!is_numeric($maxCustomVars)) { + throw new \Exception('The number of available custom variables has to be number'); + } + + $maxCustomVars = (int) $maxCustomVars; + + if ($maxCustomVars <= 0) { + throw new \Exception('There has to be at least 1 custom variable'); + } + + return $maxCustomVars; + } + + private function confirmChange(OutputInterface $output) + { + $output->writeln(''); + + $dialog = $this->getHelperSet()->get('dialog'); + return $dialog->askConfirmation( + $output, + '<question>Are you sure you want to perform these actions? (y/N)</question>', + false + ); + } + + private function printChanges($scope, $numVarsToSet, OutputInterface $output) + { + $model = new Model($scope); + $scopeName = $model->getScopeName(); + $highestIndex = $model->getHighestCustomVarIndex(); + $numCurrentCustomVars = $model->getCurrentNumCustomVars(); + $numVarsDifference = $this->getAbsoluteDifference($numCurrentCustomVars, $numVarsToSet); + + $output->writeln(''); + $output->writeln(sprintf('Scope "%s"', $scopeName)); + + if ($numVarsToSet > $numCurrentCustomVars) { + + $indexes = implode(',', range($highestIndex + 1, $highestIndex + $numVarsDifference)); + $output->writeln( + sprintf('%s new custom variables having the index(es) %s will be ADDED', $numVarsDifference, $indexes) + ); + + } elseif ($numVarsToSet < $numCurrentCustomVars) { + + $indexes = implode(',', range($highestIndex - $numVarsDifference + 1, $highestIndex)); + $output->writeln( + sprintf("%s existing custom variables having the index(es) %s will be REMOVED.", $numVarsDifference, $indexes) + ); + $output->writeln('<comment>This is an irreversible change</comment>'); + } + } + + private function getAbsoluteDifference($currentNumber, $numberToSet) + { + return abs($numberToSet - $currentNumber); + } + + private function removeCustomVariables(Model $model, $numberOfVarsToRemove, OutputInterface $output) + { + for ($index = 0; $index < $numberOfVarsToRemove; $index++) { + $indexRemoved = $model->removeCustomVariable(); + $this->progress->advance(); + $output->writeln(' <info>Removed a variable in scope "' . $model->getScopeName() . '" having the index ' . $indexRemoved . '</info>'); + } + } + + private function addCustomVariables(Model $model, $numberOfVarsToAdd, OutputInterface $output) + { + for ($index = 0; $index < $numberOfVarsToAdd; $index++) { + $indexAdded = $model->addCustomVariable(); + $this->progress->advance(); + $output->writeln(' <info>Added a variable in scope "' . $model->getScopeName() . '" having the index ' . $indexAdded . '</info>'); + } + } + + private function getNumberOfChangesToPerform($numVarsToSet) + { + $numChangesToPerform = 0; + + foreach (Model::getScopes() as $scope) { + $model = new Model($scope); + $numCurrentCustomVars = $model->getCurrentNumCustomVars(); + $numChangesToPerform += $this->getAbsoluteDifference($numCurrentCustomVars, $numVarsToSet); + } + + return $numChangesToPerform; + } +} diff --git a/plugins/CustomVariables/CustomVariables.php b/plugins/CustomVariables/CustomVariables.php index 12bc5b007b..c7e47f08e1 100644 --- a/plugins/CustomVariables/CustomVariables.php +++ b/plugins/CustomVariables/CustomVariables.php @@ -38,6 +38,7 @@ class CustomVariables extends \Piwik\Plugin 'API.getReportMetadata' => 'getReportMetadata', 'API.getSegmentDimensionMetadata' => 'getSegmentsMetadata', 'ViewDataTable.configure' => 'configureViewDataTable', + 'Console.addCommands' => 'addConsoleCommands' ); return $hooks; } @@ -52,6 +53,16 @@ class CustomVariables extends \Piwik\Plugin MenuMain::getInstance()->add('General_Visitors', 'CustomVariables_CustomVariables', array('module' => 'CustomVariables', 'action' => 'index'), $display = true, $order = 50); } + public function install() + { + Model::install(); + } + + public function addConsoleCommands(&$commands) + { + $commands[] = __NAMESPACE__ . '\\Commands\\SetNumberOfCustomVariables'; + } + /** * Returns metadata for available reports */ diff --git a/plugins/CustomVariables/Model.php b/plugins/CustomVariables/Model.php new file mode 100644 index 0000000000..992be895e5 --- /dev/null +++ b/plugins/CustomVariables/Model.php @@ -0,0 +1,124 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\CustomVariables; + +use Piwik\Common; +use Piwik\DataTable; +use Piwik\Db; + +class Model +{ + const SCOPE_PAGE = 'log_link_visit_action'; + const SCOPE_VISIT = 'log_visit'; + const SCOPE_CONVERSION = 'log_conversion'; + + private $scope = null; + + public function __construct($scope) + { + if (empty($scope) || !in_array($scope, $this->getScopes())) { + throw new \Exception('Invalid custom variable scope'); + } + + $this->scope = $scope; + } + + public function getScopeName() + { + // actually we should have a class for each scope but don't want to overengineer it for now + switch ($this->scope) { + case self::SCOPE_PAGE: + return 'Page'; + case self::SCOPE_VISIT: + return 'Visit'; + case self::SCOPE_CONVERSION: + return 'Conversion'; + } + } + + public function getCurrentNumCustomVars() + { + $customVarColumns = $this->getCustomVarColumnNames(); + + $currentNumCustomVars = count($customVarColumns) / 2; + + return (int) $currentNumCustomVars; + } + + public function getHighestCustomVarIndex() + { + $columns = $this->getCustomVarColumnNames(); + + if (empty($columns)) { + return 0; + } + + $indexes = array_map(function ($column) { + $onlyNumber = str_replace(array('custom_var_k', 'custom_var_v'), '', $column); + return (int) $onlyNumber; + }, $columns); + + return max($indexes); + } + + private function getCustomVarColumnNames() + { + $dbTable = Common::prefixTable($this->scope); + $columns = Db::getColumnNamesFromTable($dbTable); + + $customVarColumns = array_filter($columns, function ($column) { + return false !== strpos($column, 'custom_var_'); + }); + + return $customVarColumns; + } + + public function removeCustomVariable() + { + $dbTable = Common::prefixTable($this->scope); + $index = $this->getHighestCustomVarIndex(); + + if ($index < 1) { + return null; + } + + Db::exec(sprintf('ALTER TABLE %s DROP COLUMN custom_var_k%d', $dbTable, $index)); + Db::exec(sprintf('ALTER TABLE %s DROP COLUMN custom_var_v%d', $dbTable, $index)); + + return $index; + } + + public function addCustomVariable() + { + $dbTable = Common::prefixTable($this->scope); + $index = $this->getHighestCustomVarIndex() + 1; + + Db::exec(sprintf('ALTER TABLE %s ADD COLUMN custom_var_k%d VARCHAR(200) DEFAULT NULL', $dbTable, $index)); + Db::exec(sprintf('ALTER TABLE %s ADD COLUMN custom_var_v%d VARCHAR(200) DEFAULT NULL', $dbTable, $index)); + + return $index; + } + + public static function getScopes() + { + return array(self::SCOPE_PAGE, self::SCOPE_VISIT, self::SCOPE_CONVERSION); + } + + public static function install() + { + foreach (self::getScopes() as $scope) { + $model = new Model($scope); + for ($index = 0; $index < 5; $index++) { + $model->addCustomVariable(); + } + } + } + +} + diff --git a/plugins/CustomVariables/tests/ModelTest.php b/plugins/CustomVariables/tests/ModelTest.php new file mode 100644 index 0000000000..b141cb4f5d --- /dev/null +++ b/plugins/CustomVariables/tests/ModelTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CustomVariables\tests; +use Piwik\Db; +use Piwik\Plugins\CustomVariables\Model; + +/** + * @group CustomVariables + * @group ModelTest + * @group Database + */ +class ModelTest extends \DatabaseTestCase +{ + public function testGetAllScopes() + { + $this->assertEquals(array('log_link_visit_action', 'log_visit', 'log_conversion'), Model::getScopes()); + } + + public function testGetScopeName() + { + $this->assertEquals('Page', $this->getPageScope()->getScopeName()); + $this->assertEquals('Visit', $this->getVisitScope()->getScopeName()); + $this->assertEquals('Conversion', $this->getConversionScope()->getScopeName()); + } + + public function test_getCurrentNumCustomVars() + { + $this->assertEquals(5, $this->getPageScope()->getCurrentNumCustomVars()); + $this->assertEquals(5, $this->getVisitScope()->getCurrentNumCustomVars()); + $this->assertEquals(5, $this->getConversionScope()->getCurrentNumCustomVars()); + + $this->getPageScope()->addCustomVariable(); + $this->getConversionScope()->removeCustomVariable(); + + $this->assertEquals(6, $this->getPageScope()->getCurrentNumCustomVars()); + $this->assertEquals(5, $this->getVisitScope()->getCurrentNumCustomVars()); + $this->assertEquals(4, $this->getConversionScope()->getCurrentNumCustomVars()); + } + + public function test_getHighestCustomVarIndex_addCustomVariable_removeCustomVariable() + { + $this->assertEquals(5, $this->getPageScope()->getHighestCustomVarIndex()); + $this->assertEquals(5, $this->getVisitScope()->getHighestCustomVarIndex()); + $this->assertEquals(5, $this->getConversionScope()->getHighestCustomVarIndex()); + + $this->getPageScope()->addCustomVariable(); + $this->getConversionScope()->removeCustomVariable(); + + $this->assertEquals(6, $this->getPageScope()->getHighestCustomVarIndex()); + $this->assertEquals(5, $this->getVisitScope()->getHighestCustomVarIndex()); + $this->assertEquals(4, $this->getConversionScope()->getHighestCustomVarIndex()); + + $this->getConversionScope()->removeCustomVariable(); + $this->getPageScope()->addCustomVariable(); + $this->getVisitScope()->addCustomVariable(); + $this->getPageScope()->addCustomVariable(); + $this->getConversionScope()->removeCustomVariable(); + + $this->assertEquals(8, $this->getPageScope()->getHighestCustomVarIndex()); + $this->assertEquals(6, $this->getVisitScope()->getHighestCustomVarIndex()); + $this->assertEquals(2, $this->getConversionScope()->getHighestCustomVarIndex()); + } + + public function test_removeCustomVariable_shouldNotFailIfRemovesMoreThanExist() + { + $scope = $this->getPageScope(); + + $this->assertEquals(5, $scope->getHighestCustomVarIndex()); + + for ($index = 0; $index < 5; $index++) { + $scope->removeCustomVariable(); + $this->assertEquals(4 - $index, $scope->getHighestCustomVarIndex()); + } + + $this->assertNull($scope->removeCustomVariable()); + $this->assertEquals(0, $scope->getHighestCustomVarIndex()); + $this->assertEquals(0, $scope->getCurrentNumCustomVars()); + } + + public function test_removeCustomVariable_addCustomVariable_ReturnsIndex() + { + $scopeToAdd = $this->getPageScope(); + $scopeToRemove = $this->getVisitScope(); + + for ($index = 0; $index < 5; $index++) { + $this->assertEquals(5 - $index, $scopeToRemove->removeCustomVariable()); + $this->assertEquals(6 + $index, $scopeToAdd->addCustomVariable()); + } + } + + private function getPageScope() + { + return new Model(Model::SCOPE_PAGE); + } + + private function getVisitScope() + { + return new Model(Model::SCOPE_VISIT); + } + + private function getConversionScope() + { + return new Model(Model::SCOPE_CONVERSION); + } + + +} diff --git a/tests/PHPUnit/Integration/Core/DbTest.php b/tests/PHPUnit/Integration/Core/DbTest.php new file mode 100644 index 0000000000..3a8c31e7e8 --- /dev/null +++ b/tests/PHPUnit/Integration/Core/DbTest.php @@ -0,0 +1,32 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +use Piwik\Db; +use Piwik\Common; + +/** + * Class Core_DbTest + * + * @group Core + */ +class Core_DbTest extends DatabaseTestCase +{ + + public function test_getColumnNamesFromTable() + { + $this->assertColumnNames('access', array('login', 'idsite', 'access')); + $this->assertColumnNames('option', array('option_name', 'option_value', 'autoload')); + } + + private function assertColumnNames($tableName, $expectedColumnNames) + { + $colmuns = Db::getColumnNamesFromTable(Common::prefixTable($tableName)); + + $this->assertEquals($expectedColumnNames, $colmuns); + } + +} \ No newline at end of file -- GitLab