Passed
Push — master ( 15b214...6dbacf )
by Andreas
03:07
created

schema::diff()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 8.8343

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 6
eloc 13
c 3
b 0
f 0
nc 7
nop 3
dl 0
loc 22
ccs 8
cts 14
cp 0.5714
crap 8.8343
rs 9.2222
1
<?php
2
/**
3
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
4
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
6
 */
7
8
namespace midgard\portable\command;
9
10
use midgard\portable\storage\connection;
11
use midgard\portable\classgenerator;
12
use Symfony\Component\Console\Command\Command;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Console\Command\Command was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Symfony\Component\Console\Input\InputInterface;
14
use Symfony\Component\Console\Input\InputArgument;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Symfony\Component\Console\Helper\ProgressBar;
17
use Symfony\Component\Console\Question\Question;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Console\Question\Question was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use midgard_storage;
19
use midgard_connection;
20
use Doctrine\ORM\Tools\SchemaTool;
21
use Symfony\Component\Console\Input\InputOption;
22
use Doctrine\Common\Proxy\ProxyGenerator;
23
use Doctrine\DBAL\Schema\Column;
24
use Doctrine\DBAL\Schema\SchemaDiff;
25
use Doctrine\DBAL\Schema\Schema as dbal_schema;
26
27
/**
28
 * (Re)generate mapping information from MgdSchema XMLs
29
 */
30
class schema extends Command
31
{
32
    public $connected = false;
33
34
    protected function configure()
35
    {
36
        $this->setName('schema')
37
            ->setDescription('(Re)generate mapping information from MgdSchema XMLs')
38
            ->addArgument('config', InputArgument::OPTIONAL, 'Full path to midgard-portable config file')
39
            ->addOption('force', null, InputOption::VALUE_NONE, 'Ignore errors from DB')
40
            ->addOption('delete', null, InputOption::VALUE_NONE, 'Delete columns/tables that are not defined in mgdschema');
41
    }
42
43
    protected function execute(InputInterface $input, OutputInterface $output) : int
44
    {
45
        if (!$this->connected) {
46
            $path = $input->getArgument('config');
47
            if (empty($path)) {
48
                if (file_exists(OPENPSA_PROJECT_BASEDIR . 'config/midgard-portable.inc.php')) {
0 ignored issues
show
Bug introduced by
The constant midgard\portable\command\OPENPSA_PROJECT_BASEDIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
49
                    $path = OPENPSA_PROJECT_BASEDIR . 'config/midgard-portable.inc.php';
50
                } else {
51
                    $dialog = $this->getHelper('question');
52
                    $path = $dialog->ask($input, $output, new Question('<question>Enter path to config file</question>'));
53
                }
54
            }
55
            if (!file_exists($path)) {
56
                throw new \RuntimeException('Config file ' . $path . ' not found');
57
            }
58
            //we have to delay startup so that we can delete the entity class file before it gets included
59
            connection::set_autostart(false);
60
            require $path;
61
        }
62
63
        $mgd_config = midgard_connection::get_instance()->config;
64
        $mgdschema_file = $mgd_config->vardir . '/mgdschema_classes.php';
65
        if (   file_exists($mgdschema_file)
66
            && !unlink($mgdschema_file)) {
67
            throw new \RuntimeException('Could not unlink ' . $mgdschema_file);
68
        }
69
        if (connection::get_parameter('dev_mode') !== true) {
70
            $driver = connection::get_parameter('driver');
71
            $classgenerator = new classgenerator($driver->get_manager(), $mgdschema_file);
72
            $classgenerator->write($driver->get_namespace());
73
        }
74
        if (!file_exists($mgd_config->blobdir . '/0/0')) {
75
            $mgd_config->create_blobdir();
76
        }
77
        connection::startup();
78
        $em = connection::get_em();
79
        connection::invalidate_cache();
80
        $cms = $em->getMetadataFactory()->getAllMetadata();
81
82
        // create storage
83
        if (    !midgard_storage::create_base_storage()
84
             && midgard_connection::get_instance()->get_error_string() != 'MGD_ERR_OK') {
85
            throw new \Exception("Failed to create base database structures" . midgard_connection::get_instance()->get_error_string());
86
        }
87
        $force = $input->getOption('force');
88
        $to_update = [];
89
        $to_create = [];
90
91
        $sm = $em->getConnection()->createSchemaManager();
92
        foreach ($cms as $cm) {
93
            if ($sm->tablesExist([$cm->getTableName()])) {
94
                $to_update[] = $cm;
95
            } else {
96
                $to_create[] = $cm;
97
            }
98
        }
99
100
        if (!empty($to_create)) {
101
            $output->writeln('Creating <info>' . count($to_create) . '</info> new tables');
102
            $tool = new SchemaTool($em);
103
            try {
104
                $tool->createSchema($to_create);
105
            } catch (\Exception $e) {
106
                if (!$force) {
107
                    throw $e;
108
                }
109
                $output->writeln('<error>' . $e->getMessage() . '</error>');
110
            }
111
        }
112
        if (!empty($to_update)) {
113
            $delete = $input->getOption('delete');
114
            $this->process_updates($to_update, $output, $force, $delete);
115
        }
116
        $output->writeln('Generating proxies');
117
        $this->generate_proxyfiles($cms);
118
119
        $output->writeln('Done');
120
        return 0;
121
    }
122
123
    private function generate_proxyfiles(array $cms)
124
    {
125
        $em = connection::get_em();
126
        $generator = new ProxyGenerator($em->getConfiguration()->getProxyDir(), $em->getConfiguration()->getProxyNamespace());
127
        $generator->setPlaceholder('baseProxyInterface', 'Doctrine\ORM\Proxy\Proxy');
128
129
        foreach ($cms as $cm) {
130
            $filename = $generator->getProxyFileName($cm->getName());
131
            if (file_exists($filename)) {
132
                unlink($filename);
133
            }
134
            $generator->generateProxyClass($cm, $filename);
135
        }
136
    }
137
138
    /**
139
     * Since we normally don't delete old columns, we have to disable DBAL's renaming
140
     * detection, because otherwise a new column might just reuse an outdated one (keeping the values)
141
     */
142 1
    public static function diff(dbal_schema $from, dbal_schema $to, bool $delete = false) : SchemaDiff
143
    {
144 1
        $comparator = connection::get_em()->getConnection()->createSchemaManager()->createComparator();
145 1
        $diff = $comparator->compareSchemas($from, $to);
146
147 1
        foreach ($diff->changedTables as $changed_table) {
148 1
            if (!empty($changed_table->renamedColumns)) {
149
                if (empty($changed_table->addedColumns)) {
150
                    $changed_table->addedColumns = [];
151
                }
152
153
                foreach ($changed_table->renamedColumns as $name => $column) {
154
                    $changed_table->addedColumns[$column->getName()] = $column;
155
                    $changed_table->removedColumns[$name] = new Column($name, $column->getType());
156
                }
157
                $changed_table->renamedColumns = [];
158
            }
159 1
            if (!$delete) {
160 1
                $changed_table->removedColumns = [];
161
            }
162
        }
163 1
        return $diff;
164
    }
165
166
    private function process_updates(array $to_update, OutputInterface $output, $force, $delete)
167
    {
168
        $em = connection::get_em();
169
        $conn = $em->getConnection();
170
        $tool = new SchemaTool($em);
171
        $from = $conn->createSchemaManager()->createSchema();
172
        $to = $tool->getSchemaFromMetadata($to_update);
173
174
        $diff = self::diff($from, $to, $delete);
175
176
        if ($delete) {
177
            $sql = $diff->toSql($conn->getDatabasePlatform());
178
        } else {
179
            $sql = $diff->toSaveSql($conn->getDatabasePlatform());
180
        }
181
182
        if (empty($sql)) {
183
            return;
184
        }
185
186
        $output->writeln('Executing <info>' . count($sql) . '</info> updates');
187
        $progress = new ProgressBar($output);
188
        $progress->start(count($sql));
189
190
        foreach ($sql as $sql_line) {
191
            if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
192
                $output->writeln(' Executing <info>' . $sql_line . '</info>');
193
            }
194
            try {
195
                $conn->executeQuery($sql_line);
196
            } catch (\Exception $e) {
197
                if (!$force) {
198
                    throw $e;
199
                }
200
                $output->writeln('<error>' . $e->getMessage() . '</error>');
201
            }
202
203
            $progress->advance();
204
        }
205
        $progress->finish();
206
        $output->writeln('');
207
    }
208
}
209