Passed
Push — master ( c365d8...1a7141 )
by Andreas
03:30
created

schema::generate_proxyfiles()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 3
eloc 8
c 2
b 0
f 1
nc 3
nop 1
dl 0
loc 12
ccs 0
cts 9
cp 0
crap 12
rs 10
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 Doctrine\DBAL\Schema\Comparator;
22
use Symfony\Component\Console\Input\InputOption;
23
use Doctrine\Common\Proxy\ProxyGenerator;
24
use Doctrine\DBAL\Schema\Column;
25
use Doctrine\DBAL\Schema\SchemaDiff;
26
use Doctrine\DBAL\Schema\Schema as dbal_schema;
27
28
/**
29
 * (Re)generate mapping information from MgdSchema XMLs
30
 */
31
class schema extends Command
32
{
33
    public $connected = false;
34
35
    protected function configure()
36
    {
37
        $this->setName('schema')
38
            ->setDescription('(Re)generate mapping information from MgdSchema XMLs')
39
            ->addArgument('config', InputArgument::OPTIONAL, 'Full path to midgard-portable config file')
40
            ->addOption('force', null, InputOption::VALUE_NONE, 'Ignore errors from DB')
41
            ->addOption('delete', null, InputOption::VALUE_NONE, 'Delete columns/tables that are not defined in mgdschema');
42
    }
43
44
    protected function execute(InputInterface $input, OutputInterface $output) : int
45
    {
46
        if (!$this->connected) {
47
            $path = $input->getArgument('config');
48
            if (empty($path)) {
49
                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...
50
                    $path = OPENPSA_PROJECT_BASEDIR . 'config/midgard-portable.inc.php';
51
                } else {
52
                    $dialog = $this->getHelper('question');
53
                    $path = $dialog->ask($input, $output, new Question('<question>Enter path to config file</question>'));
54
                }
55
            }
56
            if (!file_exists($path)) {
57
                throw new \RuntimeException('Config file ' . $path . ' not found');
58
            }
59
            //we have to delay startup so that we can delete the entity class file before it gets included
60
            connection::set_autostart(false);
61
            require $path;
62
        }
63
64
        $mgd_config = midgard_connection::get_instance()->config;
65
        $mgdschema_file = $mgd_config->vardir . '/mgdschema_classes.php';
66
        if (   file_exists($mgdschema_file)
67
            && !unlink($mgdschema_file)) {
68
            throw new \RuntimeException('Could not unlink ' . $mgdschema_file);
69
        }
70
        if (connection::get_parameter('dev_mode') !== true) {
71
            $driver = connection::get_parameter('driver');
72
            $classgenerator = new classgenerator($driver->get_manager(), $mgdschema_file);
73
            $classgenerator->write($driver->get_namespace());
74
        }
75
        if (!file_exists($mgd_config->blobdir . '/0/0')) {
76
            $mgd_config->create_blobdir();
77
        }
78
        connection::startup();
79
        $em = connection::get_em();
80
        connection::invalidate_cache();
81
        $cms = $em->getMetadataFactory()->getAllMetadata();
82
83
        // create storage
84
        if (    !midgard_storage::create_base_storage()
85
             && midgard_connection::get_instance()->get_error_string() != 'MGD_ERR_OK') {
86
            throw new \Exception("Failed to create base database structures" . midgard_connection::get_instance()->get_error_string());
87
        }
88
        $force = $input->getOption('force');
89
        $to_update = [];
90
        $to_create = [];
91
92
        $sm = $em->getConnection()->createSchemaManager();
93
        foreach ($cms as $cm) {
94
            if ($sm->tablesExist([$cm->getTableName()])) {
95
                $to_update[] = $cm;
96
            } else {
97
                $to_create[] = $cm;
98
            }
99
        }
100
101
        if (!empty($to_create)) {
102
            $output->writeln('Creating <info>' . count($to_create) . '</info> new tables');
103
            $tool = new SchemaTool($em);
104
            try {
105
                $tool->createSchema($to_create);
106
            } catch (\Exception $e) {
107
                if (!$force) {
108
                    throw $e;
109
                }
110
                $output->writeln('<error>' . $e->getMessage() . '</error>');
111
            }
112
        }
113
        if (!empty($to_update)) {
114
            $delete = $input->getOption('delete');
115
            $this->process_updates($to_update, $output, $force, $delete);
116
        }
117
        $output->writeln('Generating proxies');
118
        $this->generate_proxyfiles($cms);
119
120
        $output->writeln('Done');
121
        return 0;
122
    }
123
124
    private function generate_proxyfiles(array $cms)
125
    {
126
        $em = connection::get_em();
127
        $generator = new ProxyGenerator($em->getConfiguration()->getProxyDir(), $em->getConfiguration()->getProxyNamespace());
128
        $generator->setPlaceholder('baseProxyInterface', 'Doctrine\ORM\Proxy\Proxy');
129
130
        foreach ($cms as $cm) {
131
            $filename = $generator->getProxyFileName($cm->getName());
132
            if (file_exists($filename)) {
133
                unlink($filename);
134
            }
135
            $generator->generateProxyClass($cm, $filename);
136
        }
137
    }
138
139
    /**
140
     * Since we normally don't delete old columns, we have to disable DBAL's renaming
141
     * detection, because otherwise a new column might just reuse an outdated one (keeping the values)
142
     */
143 1
    public static function diff(dbal_schema $from, dbal_schema $to, bool $delete = false) : SchemaDiff
144
    {
145 1
        $platform = connection::get_em()->getConnection()->getDatabasePlatform();
146 1
        $comparator = new Comparator($platform);
147 1
        $diff = $comparator->compareSchemas($from, $to);
148
149 1
        foreach ($diff->changedTables as $changed_table) {
150 1
            if (!empty($changed_table->renamedColumns)) {
151
                if (empty($changed_table->addedColumns)) {
152
                    $changed_table->addedColumns = [];
153
                }
154
155
                foreach ($changed_table->renamedColumns as $name => $column) {
156
                    $changed_table->addedColumns[$column->getName()] = $column;
157
                    $changed_table->removedColumns[$name] = new Column($name, $column->getType());
158
                }
159
                $changed_table->renamedColumns = [];
160
            }
161 1
            if (!$delete) {
162 1
                $changed_table->removedColumns = [];
163
            }
164
            // workaround for https://github.com/doctrine/dbal/issues/5369
165 1
            foreach ($changed_table->changedColumns as $name => $col_diff) {
166 1
                if (!$comparator->diffColumn($col_diff->column, $col_diff->fromColumn)) {
0 ignored issues
show
Bug introduced by
It seems like $col_diff->fromColumn can also be of type null; however, parameter $column2 of Doctrine\DBAL\Schema\Comparator::diffColumn() does only seem to accept Doctrine\DBAL\Schema\Column, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

166
                if (!$comparator->diffColumn($col_diff->column, /** @scrutinizer ignore-type */ $col_diff->fromColumn)) {
Loading history...
167 1
                    unset($changed_table->changedColumns[$name]);
168
                }
169
            }
170
        }
171 1
        return $diff;
172
    }
173
174
    private function process_updates(array $to_update, OutputInterface $output, $force, $delete)
175
    {
176
        $em = connection::get_em();
177
        $conn = $em->getConnection();
178
        $tool = new SchemaTool($em);
179
        $from = $conn->createSchemaManager()->createSchema();
180
        $to = $tool->getSchemaFromMetadata($to_update);
181
182
        $diff = self::diff($from, $to, $delete);
183
184
        if ($delete) {
185
            $sql = $diff->toSql($conn->getDatabasePlatform());
186
        } else {
187
            $sql = $diff->toSaveSql($conn->getDatabasePlatform());
188
        }
189
190
        if (empty($sql)) {
191
            return;
192
        }
193
194
        $output->writeln('Executing <info>' . count($sql) . '</info> updates');
195
        $progress = new ProgressBar($output);
196
        $progress->start(count($sql));
197
198
        foreach ($sql as $sql_line) {
199
            if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
200
                $output->writeln(' Executing <info>' . $sql_line . '</info>');
201
            }
202
            try {
203
                $conn->executeQuery($sql_line);
204
            } catch (\Exception $e) {
205
                if (!$force) {
206
                    throw $e;
207
                }
208
                $output->writeln('<error>' . $e->getMessage() . '</error>');
209
            }
210
211
            $progress->advance();
212
        }
213
        $progress->finish();
214
        $output->writeln('');
215
    }
216
}
217