Issues (134)

src/driver.php (5 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/gpl.html GNU General Public License
6
 */
7
8
namespace midgard\portable;
9
10
use midgard\portable\mgdschema\manager;
11
use midgard\portable\mgdschema\translator;
12
use midgard\portable\mgdschema\property;
13
use Doctrine\Persistence\Mapping\Driver\MappingDriver as driver_interface;
14
use Doctrine\Persistence\Mapping\ClassMetadata;
15
use Doctrine\ORM\Mapping\MappingException;
16
use Doctrine\ORM\Mapping\ClassMetadata as CM;
17
use Doctrine\DBAL\Types\Types;
18
19
class driver implements driver_interface
20
{
21
    private array $dbtypemap = [
22
        'unsigned integer' => ['type' => Types::INTEGER, 'default' => 0], // <== UNSIGNED in Doctrine\DBAL\Schema\Column
23
        'integer' => ['type' => Types::INTEGER, 'default' => 0],
24
        'boolean' => ['type' => Types::BOOLEAN, 'default' => false],
25
        'bool' => ['type' => Types::BOOLEAN, 'default' => false],
26
        'guid' => ['type' => Types::STRING, 'length' => 80, 'default' => ''],
27
        'varchar(80)' => ['type' => Types::STRING, 'length' => 80, 'default' => ''],
28
        'string' => ['type' => Types::STRING, 'length' => 255, 'default' => ''],
29
        'datetime' => ['type' => Types::DATETIME_MUTABLE, 'default' => '0001-01-01 00:00:00'],
30
        'date' => ['type' => Types::DATE_MUTABLE, 'default' => '0001-01-01'],
31
        'text' => ['type' => Types::TEXT],
32
        'longtext' => ['type' => Types::TEXT],
33
        'float' => ['type' => Types::FLOAT, 'default' => 0.0],
34
        'double' => ['type' => Types::FLOAT, 'default' => 0.0]
35
    ];
36
37
    private readonly string $vardir;
38
39
    private ?array $types = null;
40
41
    private readonly string $namespace;
42
43
    private manager $manager;
44
45
    /**
46
     * keep track of the namespaces already in use and
47
     * remember the used manager instance for resolving types
48
     */
49
    private static array $processed_namespaces = [];
50
51
    /**
52
     * indicates whether the current namespace has been used before
53
     */
54
    private readonly bool $is_fresh_namespace;
55
56 14
    public function __construct(array $schemadirs, string $vardir, string $namespace = 'midgard\\portable\\entities')
57
    {
58 14
        $this->vardir = $vardir . '/';
0 ignored issues
show
The property vardir is declared read-only in midgard\portable\driver.
Loading history...
59 14
        $this->namespace = $namespace;
0 ignored issues
show
The property namespace is declared read-only in midgard\portable\driver.
Loading history...
60
61 14
        $this->is_fresh_namespace = !isset(self::$processed_namespaces[$this->namespace]);
0 ignored issues
show
The property is_fresh_namespace is declared read-only in midgard\portable\driver.
Loading history...
62 14
        if ($this->is_fresh_namespace) {
63 14
            $this->manager = new manager($schemadirs, $this->namespace);
64 14
            self::$processed_namespaces[$this->namespace] = ["manager" => $this->manager];
65
        } else {
66
            // reuse manager instance
67
            $this->manager = self::$processed_namespaces[$this->namespace]["manager"];
68
        }
69
    }
70
71 7
    public function is_fresh_namespace() : bool
72
    {
73 7
        return $this->is_fresh_namespace;
74
    }
75
76 7
    public function get_namespace() : string
77
    {
78 7
        return $this->namespace;
79
    }
80
81 8
    public function get_manager() : manager
82
    {
83 8
        return $this->manager;
84
    }
85
86 10
    public function get_vardir() : string
87
    {
88 10
        return rtrim($this->vardir, '/');
89
    }
90
91 17
    private function initialize()
92
    {
93 17
        $this->types ??= $this->manager->get_types();
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     */
99 3
    public function getAllClassNames() : array
100
    {
101 3
        $this->initialize();
102 3
        return array_keys($this->types);
103
    }
104
105
    /**
106
     * {@inheritDoc}
107
     */
108 13
    public function isTransient($classname) : bool
109
    {
110 13
        $this->initialize();
111 13
        return !isset($this->types[$classname]);
112
    }
113
114
    /**
115
     * {@inheritDoc}
116
     */
117 16
    public function loadMetadataForClass($classname, ClassMetadata $metadata)
118
    {
119 16
        $this->initialize();
120 16
        if (!isset($this->types[$classname])) {
121 4
            throw MappingException::classIsNotAValidEntityOrMappedSuperClass($classname);
122
        }
123
124 15
        $type = $this->types[$classname];
125
126
        // TODO: extends
127
128 15
        $table = [
129 15
            'name' => '`' . $type->table . '`',
130 15
            'options' => [
131
                //Doctrine's default on MySQL is InnoDB, and the foreign keys don't play well with Midgard logic
132
                //TODO: Maybe at some point we could try to figure out how to explicitly disable foreign key constraint creation instead
133 15
                'engine' => 'MyISAM'
134 15
            ]
135 15
        ];
136
137 15
        $metadata->setPrimaryTable($table);
138
139 15
        $properties = $type->get_properties();
140 15
        $metadata->midgard['parent'] = $type->parent;
0 ignored issues
show
Accessing midgard on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
141 15
        $metadata->midgard['parentfield'] = $type->parentfield;
142 15
        $metadata->midgard['upfield'] = $type->upfield;
143 15
        $metadata->midgard['childtypes'] = $this->manager->get_child_classes($type->name);
144 15
        $metadata->midgard['field_aliases'] = $type->field_aliases;
145 15
        $metadata->midgard['links_as_entities'] = $type->links_as_entities;
146 15
        $metadata->midgard['field_order'] = array_keys($properties);
147
148 15
        foreach ($properties as $name => $property) {
149
            // doctrine can handle id links only
150 15
            if (   $property->link
151 15
                && $target_class = $this->manager->resolve_targetclass($property)) {
152 11
                $link_mapping = [
153 11
                    'fieldName' => $property->name,
154 11
                    'targetEntity' => $target_class,
155 11
                    'joinColumns' => [
156 11
                        [
157 11
                            'name' => $property->field,
158 11
                            'referencedColumnName' => $property->link['field']
159 11
                        ]
160 11
                    ],
161 11
                    'midgard:link_target' => $property->link['field'],
162 11
                    'midgard:link_name' => $property->link['target'],
163 11
                ];
164
165 11
                if ($link_mapping['fieldName'] == 'id') {
166
                    $link_mapping['id'] = true;
167
                }
168
169 11
                $metadata->mapManyToOne($link_mapping);
170 11
                continue;
171
            }
172
173 15
            $mapping = $this->dbtypemap[$property->dbtype] ?? $this->parse_dbtype($property);
174
175 15
            if ($property->unique) {
176 15
                if ($property->name == 'guid') {
177 15
                    $mapping['unique'] = true;
178
                } else {
179
                    //we can't set this as a real DB constraint because of softdelete and tree hierarchies
180 7
                    $metadata->midgard['unique_fields'][] = $property->name;
181
                }
182
            }
183
184 15
            $mapping['columnName'] = $property->field;
185 15
            $mapping['midgard:midgard_type'] = translator::to_constant($property->type);
186 15
            $mapping['midgard:description'] = $property->description;
187
188
            // its some other link (like guid link)
189 15
            if ($property->noidlink) {
190 5
                $mapping['noidlink'] = $property->noidlink;
191
            }
192
193 15
            if ($property->name == $type->primaryfield) {
194 15
                $mapping['id'] = true;
195 15
                unset($mapping['default']);
196 15
                if ($mapping['type'] == Types::INTEGER) {
197 15
                    $metadata->setIdGeneratorType(CM::GENERATOR_TYPE_AUTO);
198
                } else {
199 4
                    $metadata->setIdGeneratorType(CM::GENERATOR_TYPE_NONE);
200
                }
201
            }
202
203 15
            $mapping['fieldName'] = $name;
204
205 15
            $metadata->mapField($mapping);
206
207 15
            if ($property->index) {
208 14
                $metadata->table['indexes'] ??= [];
0 ignored issues
show
Accessing table on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
209 14
                $metadata->table['indexes'][$type->name . '_' . $property->name . '_idx'] = ['columns' => [$property->field]];
210
            }
211
        }
212
    }
213
214 6
    private function parse_dbtype(property $property) : array
215
    {
216 6
        if (str_starts_with($property->dbtype, 'varchar')) {
217 6
            $mapping = [
218 6
                'type' => Types::STRING,
219 6
            ];
220
221 6
            if (str_ends_with($property->dbtype, ')')) {
222 5
                $mapping['length'] = (int) substr($property->dbtype, 8, -1);
223 5
                return $mapping;
224
            }
225
226 2
            if (str_ends_with($property->dbtype, ') binary')) {
227
                // see http://www.doctrine-project.org/jira/browse/DDC-1817
228 2
                $mapping['length'] = (int) substr($property->dbtype, 8, -1);
229 2
                $mapping['comment'] = 'BINARY';
230 2
                return $mapping;
231
            }
232 2
        } elseif (str_starts_with($property->dbtype, 'set')) {
233
            // see http://docs.doctrine-project.org/en/latest/cookbook/mysql-enums.html
234 2
            if (!empty($this->dbtypemap[$property->type])) {
235 2
                $mapping = $this->dbtypemap[$property->type];
236 2
                $mapping['comment'] = $property->dbtype;
237 2
                return $mapping;
238
            }
239
        } elseif (str_starts_with(strtolower($property->dbtype), 'decimal')) {
240
            $matches = [];
241
            preg_match('/DECIMAL\((\d+),(\d+)\)/i', $property->dbtype, $matches);
242
            return [
243
                'type' => Types::DECIMAL,
244
                'precision' => $matches[1],
245
                'scale' => $matches[2]
246
            ];
247
        }
248
249
        throw new \Exception($property->get_parent()->name . ': ' . $property->name . ' ' . $property->dbtype . ' not implemented yet');
250
    }
251
}
252