Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
39 / 39 |
| SchemaDefDriver | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
18 | |
100.00% |
39 / 39 |
| process | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
| loadMetadataForClass | |
100.00% |
1 / 1 |
14 | |
100.00% |
34 / 34 |
|||
| isTransient | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| kv_to_name_col | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| 1 | <?php |
| 2 | |
| 3 | // {{{ License |
| 4 | // This file is part of GNU social - https://www.gnu.org/software/social |
| 5 | // |
| 6 | // GNU social is free software: you can redistribute it and/or modify |
| 7 | // it under the terms of the GNU Affero General Public License as published by |
| 8 | // the Free Software Foundation, either version 3 of the License, or |
| 9 | // (at your option) any later version. |
| 10 | // |
| 11 | // GNU social is distributed in the hope that it will be useful, |
| 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | // GNU Affero General Public License for more details. |
| 15 | // |
| 16 | // You should have received a copy of the GNU Affero General Public License |
| 17 | // along with GNU social. If not, see <http://www.gnu.org/licenses/>. |
| 18 | // }}} |
| 19 | |
| 20 | /** |
| 21 | * Compiler pass which triggers Symfony to tell Doctrine to |
| 22 | * use our `SchemaDef` metadata driver |
| 23 | * |
| 24 | * @package GNUsocial |
| 25 | * @category DB |
| 26 | * |
| 27 | * @author Hugo Sales <hugo@hsal.es> |
| 28 | * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org |
| 29 | * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later |
| 30 | */ |
| 31 | |
| 32 | namespace App\DependencyInjection\Compiler; |
| 33 | |
| 34 | use Doctrine\Persistence\Mapping\ClassMetadata; |
| 35 | use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver; |
| 36 | use Functional as F; |
| 37 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; |
| 38 | use Symfony\Component\DependencyInjection\ContainerBuilder; |
| 39 | use Symfony\Component\DependencyInjection\Reference; |
| 40 | |
| 41 | /** |
| 42 | * Register a new ORM driver to allow use to use the old (and better) schemaDef format |
| 43 | */ |
| 44 | class SchemaDefDriver extends StaticPHPDriver implements CompilerPassInterface |
| 45 | { |
| 46 | /** |
| 47 | * Register `app.schemadef_driver` (this class instantiated with argument src/Entity) as a metadata driver |
| 48 | * |
| 49 | * @codeCoverageIgnore |
| 50 | */ |
| 51 | public function process(ContainerBuilder $container) |
| 52 | { |
| 53 | $container->findDefinition('doctrine.orm.default_metadata_driver') |
| 54 | ->addMethodCall('addDriver', |
| 55 | [new Reference('app.schemadef_driver'), 'App\\Entity'] |
| 56 | ); |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * V2 DB type => Doctrine type |
| 61 | */ |
| 62 | private const types = [ |
| 63 | 'varchar' => 'string', |
| 64 | 'char' => 'string', // char is a fixed witdh varchar |
| 65 | 'int' => 'integer', |
| 66 | 'serial' => 'integer', |
| 67 | 'tinyint' => 'smallint', // no portable tinyint |
| 68 | 'bigint' => 'bigint', |
| 69 | 'bool' => 'boolean', |
| 70 | 'numeric' => 'decimal', |
| 71 | 'text' => 'text', |
| 72 | 'datetime' => 'datetime', |
| 73 | 'timestamp' => 'datetime', |
| 74 | 'phone_number' => 'phone_number', |
| 75 | // Unused in V2, but might start being used |
| 76 | 'date' => 'date', |
| 77 | 'time' => 'time', |
| 78 | 'datetimez' => 'datetimez', |
| 79 | 'object' => 'object', |
| 80 | 'array' => 'array', |
| 81 | 'simplearray' => 'simplearray', |
| 82 | 'json_array' => 'json_array', |
| 83 | 'float' => 'float', |
| 84 | 'guid' => 'guid', |
| 85 | 'blob' => 'blob', |
| 86 | ]; |
| 87 | |
| 88 | /** |
| 89 | * Fill in the database $metadata for $class_name |
| 90 | * |
| 91 | * @param string $class_name |
| 92 | * @param ClassMetadata $metadata |
| 93 | */ |
| 94 | public function loadMetadataForClass($class_name, ClassMetadata $metadata) |
| 95 | { |
| 96 | $schema = $class_name::schemaDef(); |
| 97 | |
| 98 | $metadata->setPrimaryTable([ |
| 99 | 'name' => $schema['name'], |
| 100 | 'indexes' => self::kv_to_name_col($schema['indexes'] ?? []), |
| 101 | 'uniqueConstraints' => self::kv_to_name_col($schema['unique keys'] ?? []), |
| 102 | 'options' => ['comment' => $schema['description'] ?? ''], |
| 103 | ]); |
| 104 | |
| 105 | foreach ($schema['fields'] as $name => $opts) { |
| 106 | $unique = null; |
| 107 | foreach ($schema['unique keys'] ?? [] as $key => $uniq_arr) { |
| 108 | if (in_array($name, $uniq_arr)) { |
| 109 | $unique = $key; |
| 110 | break; |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | if (false && $opts['foreign key'] ?? false) { |
| 115 | // @codeCoverageIgnoreStart |
| 116 | // TODO: Get foreign keys working |
| 117 | foreach (['target', 'multiplicity'] as $f) { |
| 118 | if (!isset($opts[$f])) { |
| 119 | throw new \Exception("{$class_name}.{$name} doesn't have the required field `{$f}`"); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // See Doctrine\ORM\Mapping::associationMappings |
| 124 | |
| 125 | // TODO still need to map nullability, comment, fk name and such, but |
| 126 | // the interface doesn't seem to support it currently |
| 127 | [$target_entity, $target_field] = explode('.', $opts['target']); |
| 128 | $map = [ |
| 129 | 'fieldName' => $name, |
| 130 | 'targetEntity' => $target_entity, |
| 131 | 'joinColumns' => [[ |
| 132 | 'name' => $name, |
| 133 | 'referencedColumnName' => $target_field, |
| 134 | ]], |
| 135 | 'id' => in_array($name, $schema['primary key']), |
| 136 | 'unique' => $unique, |
| 137 | ]; |
| 138 | |
| 139 | switch ($opts['multiplicity']) { |
| 140 | case 'one to one': |
| 141 | $metadata->mapOneToOne($map); |
| 142 | break; |
| 143 | case 'many to one': |
| 144 | $metadata->mapManyToOne($map); |
| 145 | break; |
| 146 | case 'one to many': |
| 147 | $map['mappedBy'] = $target_field; |
| 148 | $metadata->mapOneToMany($map); |
| 149 | break; |
| 150 | case 'many to many': |
| 151 | $metadata->mapManyToMany($map); |
| 152 | break; |
| 153 | default: |
| 154 | throw new \Exception("Invalid multiplicity specified: '${opts['multiplicity']}' in class: {$class_name}"); |
| 155 | } |
| 156 | // @codeCoverageIgnoreEnd |
| 157 | } else { |
| 158 | // Convert old to new types |
| 159 | // For ints, prepend the size (smallint) |
| 160 | // The size field doesn't exist otherwise |
| 161 | $type = self::types[($opts['size'] ?? '') . $opts['type']]; |
| 162 | $default = $opts['default'] ?? null; |
| 163 | |
| 164 | $field = [ |
| 165 | // boolean, optional |
| 166 | 'id' => in_array($name, $schema['primary key']), |
| 167 | // string |
| 168 | 'fieldName' => $name, |
| 169 | // string |
| 170 | 'type' => $type, |
| 171 | // string, optional |
| 172 | 'unique' => $unique, |
| 173 | // String length, ignored if not a string |
| 174 | // int, optional |
| 175 | 'length' => $opts['length'] ?? null, |
| 176 | // boolean, optional |
| 177 | 'nullable' => !($opts['not null'] ?? false), |
| 178 | // Numeric precision and scale, ignored if not a number |
| 179 | // integer, optional |
| 180 | 'precision' => $opts['precision'] ?? null, |
| 181 | // integer, optional |
| 182 | 'scale' => $opts['scale'] ?? null, |
| 183 | 'options' => [ |
| 184 | 'comment' => $opts['description'] ?? null, |
| 185 | 'default' => $default, |
| 186 | 'unsigned' => $opts['unsigned'] ?? null, |
| 187 | // bool, optional |
| 188 | 'fixed' => $opts['type'] === 'char', |
| 189 | // 'collation' => string, unused |
| 190 | // 'check', unused |
| 191 | ], |
| 192 | // 'columnDefinition', unused |
| 193 | ]; |
| 194 | // The optional feilds from earlier were populated with null, remove them |
| 195 | $field = array_filter($field, F\not('is_null')); |
| 196 | $field['options'] = array_filter($field['options'], F\not('is_null')); |
| 197 | |
| 198 | $metadata->mapField($field); |
| 199 | if ($opts['type'] === 'serial') { |
| 200 | $metadata->setIdGeneratorType($metadata::GENERATOR_TYPE_AUTO); |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Override StaticPHPDriver's method, |
| 208 | * we care about classes that have the method `schemaDef`, |
| 209 | * instead of `loadMetadata`. |
| 210 | * |
| 211 | * @param string $class_name |
| 212 | * |
| 213 | * @return bool |
| 214 | */ |
| 215 | public function isTransient($class_name) |
| 216 | { |
| 217 | return !method_exists($class_name, 'schemaDef'); |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * Convert [$key => $val] to ['name' => $key, 'columns' => $val] |
| 222 | * |
| 223 | * @param array $arr |
| 224 | * |
| 225 | * @return array |
| 226 | */ |
| 227 | private static function kv_to_name_col(array $arr): array |
| 228 | { |
| 229 | $res = []; |
| 230 | foreach ($arr as $name => $cols) { |
| 231 | $res[] = ['name' => $name, 'columns' => $cols]; |
| 232 | } |
| 233 | return $res; |
| 234 | } |
| 235 | } |