Issues (134)

src/command/schema.php (2 issues)

Labels
Severity
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;
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;
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 bool $connected = false;
33
34 1
    protected function configure()
35
    {
36 1
        $this->setName('schema')
37 1
            ->setDescription('(Re)generate mapping information from MgdSchema XMLs')
38 1
            ->addArgument('config', InputArgument::OPTIONAL, 'Full path to midgard-portable config file')
39 1
            ->addOption('force', null, InputOption::VALUE_NONE, 'Ignore errors from DB')
40 1
            ->addOption('delete', null, InputOption::VALUE_NONE, 'Delete columns/tables that are not defined in mgdschema');
41
    }
42
43 1
    protected function execute(InputInterface $input, OutputInterface $output) : int
44
    {
45 1
        if (!$this->connected) {
46 1
            $path = $input->getArgument('config');
47 1
            if (empty($path)) {
48
                if (file_exists(OPENPSA_PROJECT_BASEDIR . 'config/midgard-portable.inc.php')) {
0 ignored issues
show
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 1
            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 1
            connection::set_autostart(false);
60 1
            require $path;
61
        }
62
63 1
        $mgd_config = midgard_connection::get_instance()->config;
64 1
        $mgdschema_file = $mgd_config->vardir . '/mgdschema_classes.php';
65 1
        if (   file_exists($mgdschema_file)
66 1
            && !unlink($mgdschema_file)) {
67
            throw new \RuntimeException('Could not unlink ' . $mgdschema_file);
68
        }
69 1
        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 1
        if (!file_exists($mgd_config->blobdir . '/0/0')) {
75 1
            $mgd_config->create_blobdir();
0 ignored issues
show
The method create_blobdir() does not exist on null. ( Ignorable by Annotation )

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

75
            $mgd_config->/** @scrutinizer ignore-call */ 
76
                         create_blobdir();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
76
        }
77 1
        connection::startup();
78 1
        $em = connection::get_em();
79 1
        connection::invalidate_cache();
80 1
        $cms = $em->getMetadataFactory()->getAllMetadata();
81
82
        // create storage
83 1
        if (    !midgard_storage::create_base_storage()
84 1
             && 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 1
        $force = $input->getOption('force');
88 1
        $to_update = [];
89 1
        $to_create = [];
90
91 1
        $sm = $em->getConnection()->createSchemaManager();
92 1
        foreach ($cms as $cm) {
93 1
            if ($sm->tablesExist([$cm->getTableName()])) {
94 1
                $to_update[] = $cm;
95
            } else {
96 1
                $to_create[] = $cm;
97
            }
98
        }
99
100 1
        if (!empty($to_create)) {
101 1
            $output->writeln('Creating <info>' . count($to_create) . '</info> new tables');
102 1
            $tool = new SchemaTool($em);
103
            try {
104 1
                $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 1
        if (!empty($to_update)) {
113 1
            $delete = $input->getOption('delete');
114 1
            $this->process_updates($to_update, $output, $force, $delete);
115
        }
116 1
        $output->writeln('Generating proxies');
117 1
        $this->generate_proxyfiles($cms);
118
119 1
        $output->writeln('Done');
120 1
        return Command::SUCCESS;
121
    }
122
123 1
    private function generate_proxyfiles(array $cms)
124
    {
125 1
        $em = connection::get_em();
126 1
        $generator = new ProxyGenerator($em->getConfiguration()->getProxyDir(), $em->getConfiguration()->getProxyNamespace());
127 1
        $generator->setPlaceholder('baseProxyInterface', 'Doctrine\ORM\Proxy\Proxy');
128
129 1
        foreach ($cms as $cm) {
130 1
            $filename = $generator->getProxyFileName($cm->getName());
131 1
            if (file_exists($filename)) {
132 1
                unlink($filename);
133
            }
134 1
            $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 2
    public static function diff(dbal_schema $from, dbal_schema $to, bool $delete = false) : SchemaDiff
143
    {
144 2
        $comparator = connection::get_em()->getConnection()->createSchemaManager()->createComparator();
145 2
        $diff = $comparator->compareSchemas($from, $to);
146
147 2
        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 2
        if (!$delete) {
164 2
            $diff->removedTables = [];
165
        }
166 2
        return $diff;
167
    }
168
169 1
    private function process_updates(array $to_update, OutputInterface $output, $force, $delete)
170
    {
171 1
        $em = connection::get_em();
172 1
        $conn = $em->getConnection();
173 1
        $tool = new SchemaTool($em);
174 1
        $from = $conn->createSchemaManager()->introspectSchema();
175 1
        $to = $tool->getSchemaFromMetadata($to_update);
176
177 1
        $diff = self::diff($from, $to, $delete);
178
179 1
        $sql = $conn->getDatabasePlatform()->getAlterSchemaSQL($diff);
180
181 1
        if (empty($sql)) {
182 1
            return;
183
        }
184
185
        $output->writeln('Executing <info>' . count($sql) . '</info> updates');
186
        $progress = new ProgressBar($output);
187
        $progress->start(count($sql));
188
189
        foreach ($sql as $sql_line) {
190
            if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
191
                $output->writeln(' Executing <info>' . $sql_line . '</info>');
192
            }
193
            try {
194
                $conn->executeQuery($sql_line);
195
            } catch (\Exception $e) {
196
                if (!$force) {
197
                    throw $e;
198
                }
199
                $output->writeln('<error>' . $e->getMessage() . '</error>');
200
            }
201
202
            $progress->advance();
203
        }
204
        $progress->finish();
205
        $output->writeln('');
206
    }
207
}
208