Passed
Push — master ( 625be7...d965a1 )
by Bruno
08:58
created

endsWith()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
ccs 0
cts 0
cp 0
cc 1
nc 1
nop 2
crap 2
1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Laravel\Targets;
4
5
use GraphQL\Type\Definition\ListOfType;
6
use GraphQL\Type\Definition\NonNull;
7
use GraphQL\Type\Definition\ObjectType;
8
use GraphQL\Type\Definition\Type;
9
use Modelarium\Exception\Exception;
10
use Modelarium\GeneratedCollection;
11
use Modelarium\GeneratedItem;
12
13
function endsWith($haystack, $needle)
14
{
15
    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
16
}
17
class MigrationGenerator extends BaseGenerator
18
{
19
    public static $counter = 0;
20
21
    /**
22
     * @var ObjectType
23
     */
24
    protected $type = null;
25 5
26
    /**
27 5
     * @var GeneratedCollection
28 5
     */
29 5
    protected $collection = null;
30 5
31 5
    public function generate(): GeneratedCollection
32
    {
33 5
        $this->collection = new GeneratedCollection();
34 5
        $item = new GeneratedItem(
35
            GeneratedItem::TYPE_MIGRATION,
36
            $this->generateString(),
37 15
            $this->getGenerateFilename($this->lowerName)
38
        );
39
        $this->collection->prepend($item);
40
        return $this->collection;
41 15
    }
42 15
43
    protected function processBasetype(
44
        \GraphQL\Type\Definition\FieldDefinition $field,
45
        \GraphQL\Language\AST\NodeList $directives
46 15
    ): array {
47 15
        $fieldName = $field->name;
48
        $extra = [];
49 1
50
        // TODO: scalars
51
52 15
        if ($field->type instanceof NonNull) {
53
            $type = $field->type->getWrappedType();
54 15
        } else {
55
            $type = $field->type;
56 15
        }
57 15
58 15
        $basetype = $type->name; /** @phpstan-ignore-line */
59 10
60 10
        $base = '';
61 10
        switch ($basetype) {
62 2
        case Type::ID:
63 2
            $base = '$table->bigIncrements("id")';
64 2
            break;
65 1
        case Type::STRING:
66 1
            $base = '$table->string("' . $fieldName . '")';
67 1
            break;
68 1
        case Type::INT:
69 1
            $base = '$table->integer("' . $fieldName . '")';
70 1
            break;
71
        case Type::BOOLEAN:
72
            $base = '$table->bool("' . $fieldName . '")';
73
            break;
74
        case Type::FLOAT:
75
            $base = '$table->float("' . $fieldName . '")';
76
            break;
77
        case 'datetime':
78
            $base = '$table->dateTime("' . $fieldName . '")';
79
        break;
80
        case 'url':
81
            $base = '$table->string("' . $fieldName . '")';
82 15
        break;
83 1
        default:
84
            throw new Exception("Unsupported type $basetype for {$field->name}");
85
            // $base = '$table->' . $basetype . '("' . $fieldName . '")';
86 15
        }
87 2
        
88 2
        if (!($field->type instanceof NonNull)) {
89 2
            $base .= '->nullable()';
90 1
        }
91 1
        
92 1
        foreach ($directives as $directive) {
93
            $name = $directive->name->value;
94
            switch ($name) {
95 1
            case 'uniqueIndex':
96 1
                $extra[] = '$table->unique("' . $fieldName . '");';
97 1
                break;
98
            case 'index':
99
                $extra[] = '$table->index("' . $fieldName . '");';
100
                break;
101
            case 'unsigned':
102
                $base .= '->unsigned()';
103
                break;
104 15
            case 'defaultValue':
105
                $x = ''; // TODO
106 15
                $base .= '->default(' . $x . ')';
107 15
                break;
108
            }
109
        }
110 5
        $base .= ';';
111
112
        array_unshift($extra, $base);
113
        return $extra;
114 5
    }
115 5
116
    protected function processRelationship(
117 5
        \GraphQL\Type\Definition\FieldDefinition $field,
118 5
        \GraphQL\Language\AST\NodeList $directives
119
    ): array {
120 1
        $lowerName = mb_strtolower($this->inflector->singularize($field->name));
121
        $lowerNamePlural = $this->inflector->pluralize($lowerName);
122
123 5
        if ($field->type instanceof NonNull) {
124
            $type = $field->type->getWrappedType();
125
        } else {
126
            $type = $field->type;
127 5
        }
128
129 5
        if ($field->type instanceof ListOfType) {
130
            $type = $field->type->getWrappedType();
131 5
        }
132 5
133
        $typeName = $type->name; /** @phpstan-ignore-line */
134 5
135 5
        $fieldName = $lowerName . '_id';
136 5
        
137 5
        $base = null;
138
        $extra = [];
139
140 5
        foreach ($directives as $directive) {
141
            $name = $directive->name->value;
142
            switch ($name) {
143 5
            case 'uniqueIndex':
144 4
                $extra[] = '$table->unique("' . $fieldName . '");';
145 4
                break;
146
            case 'index':
147 4
                $extra[] = '$table->index("' . $fieldName . '");';
148
                break;
149
            case 'belongsTo':
150
                $targetType = $this->parser->getType($typeName);
151 4
                if (!$targetType) {
152 1
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->name}");
153 1
                } elseif (!($targetType instanceof ObjectType)) {
154
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->name}");
155
                }
156 4
                try {
157 4
                    $targetField = $targetType->getField($this->lowerName); // TODO: might have another name than lowerName
158 4
                } catch (\GraphQL\Error\InvariantViolation $e) {
159 4
                    $targetField = $targetType->getField($this->lowerNamePlural);
160 1
                }
161 4
162 4
                $targetDirectives = $targetField->astNode->directives;
163
                foreach ($targetDirectives as $targetDirective) {
164
                    switch ($targetDirective->name->value) {
165 4
                    case 'hasOne':
166 5
                    case 'hasMany':
167 1
                        $base = '$table->unsignedBigInteger("' . $fieldName . '")';
168 1
                    break;
169
                    }
170
                }
171 1
                break;
172 1
            case 'belongsToMany':
173 1
                $type1 = $this->lowerName;
174
                $type2 = $lowerName;
175 1
176 4
                // we only generate once, so use a comparison for that
177 4
                if (strcasecmp($type1, $type2) < 0) {
178 4
                    $item = $this->generateManyToManyTable($type1, $type2);
179 4
                    $this->collection->push($item);
180 4
                }
181 4
                break;
182
            case 'foreign':
183
                $references = 'id';
184
                $on = $lowerNamePlural;
185
                $onDelete = null;
186 4
                $onUpdate = null;
187
                foreach ($directive->arguments as $arg) {
188 4
                    /**
189 4
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
190
                     */
191
192 4
                    $value = $arg->value->value;
0 ignored issues
show
Bug introduced by
Accessing value on the interface GraphQL\Language\AST\ValueNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
193
194
                    switch ($arg->name->value) {
195 4
                    case 'references':
196 4
                        $references = $value;
197 4
                    break;
198 4
                    case 'on':
199 4
                        $on = $value;
200 4
                    break;
201
                    case 'onDelete':
202
                        $onDelete = $value;
203 4
                    break;
204 4
                    case 'onUpdate':
205 4
                        $onUpdate = $value;
206 4
                    break;
207 4
                    }
208 4
                }
209 4
                $extra[] = '$table->foreign("' . $fieldName . '")' .
210
                    "->references(\"$references\")" .
211
                    "->on(\"$on\")" .
212
                    ($onDelete ? "->onDelete(\"$onDelete\")" : '') .
213 5
                    ($onUpdate ? "->onUpdate(\"$onUpdate\")" : '') .
214 1
                    ';';
215
                break;
216
            }
217 5
        }
218 4
219 4
        if (!($field->type instanceof NonNull)) {
220
            $base .= '->nullable()';
221
        }
222 5
223
        if ($base) {
224
            $base .= ';';
225 15
            array_unshift($extra, $base);
226
        }
227
228 15
        return $extra;
229
    }
230 15
231 5
    protected function processDirectives(
232 5
        \GraphQL\Language\AST\NodeList $directives
233 5
    ): array {
234 3
        $db = [];
235 3
236 5
        foreach ($directives as $directive) {
237 1
            $name = $directive->name->value;
238
            switch ($name) {
239 1
            case 'softDeletesDB':
240 1
                $db[] = '$table->softDeletes();';
241 1
                break;
242
            case 'index':
243 1
                $values = $directive->arguments[0]->value->values;
244 1
                
245 4
                $indexFields = [];
246 1
                foreach ($values as $value) {
247 1
                    $indexFields[] = $value->value;
248 3
                }
249 3
                $db[] = '$table->index("' . implode('", "', $indexFields) .'");';
250 3
                break;
251 3
            case 'spatialIndex':
252 3
                $db[] = '$table->spatialIndex("' . $directive->arguments[0]->value->value .'");';
253 3
                break;
254
            case 'rememberToken':
255
                $db[] = '$table->rememberToken();';
256
                break;
257
            case 'timestamps':
258 15
                $db[] = '$table->timestamps();';
259
                break;
260
            default:
261 15
            }
262
        }
263
264 15
        return $db;
265
    }
266 15
267 15
    public function generateString(): string
268
    {
269 15
        return $this->stubToString('migration', function ($stub) {
270 15
            $db = [];
271 15
272 15
            foreach ($this->type->getFields() as $field) {
273
                $directives = $field->astNode->directives;
274
                if (
275
                    ($field->type instanceof ObjectType) ||
276 5
                    ($field->type instanceof NonNull) && (
277
                        ($field->type->getWrappedType() instanceof ObjectType) ||
278 15
                        ($field->type->getWrappedType() instanceof ListOfType)
279
                    )
280
                ) {
281
                    // relationship
282 15
                    $db = array_merge($db, $this->processRelationship($field, $directives));
283
                } else {
284
                    $db = array_merge($db, $this->processBasetype($field, $directives));
285
                }
286 15
            }
287 15
288 15
            assert($this->type->astNode !== null);
289
            /**
290
             * @var \GraphQL\Language\AST\NodeList|null
291 15
             */
292 15
            $directives = $this->type->astNode->directives;
293 15
            if ($directives) {
294 15
                $db = array_merge($db, $this->processDirectives($directives));
0 ignored issues
show
Bug introduced by
$directives of type GraphQL\Language\AST\DirectiveNode[] is incompatible with the type GraphQL\Language\AST\NodeList expected by parameter $directives of Modelarium\Laravel\Targe...or::processDirectives(). ( Ignorable by Annotation )

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

294
                $db = array_merge($db, $this->processDirectives(/** @scrutinizer ignore-type */ $directives));
Loading history...
295
            }
296
297 15
            $stub = str_replace(
298 15
                '// dummyCode',
299 15
                join("\n            ", $db),
300 15
                $stub
301
            );
302
303 15
            $stub = str_replace(
304 15
                'dummytablename',
305 15
                $this->lowerNamePlural,
306 15
                $stub
307
            );
308 15
309 15
            $stub = str_replace(
310
                'modelSchemaCode',
311
                "# start graphql\n" .
312 1
                \GraphQL\Language\Printer::doPrint($this->type->astNode),
313
                "# end graphql\n" .
314
                $stub
315
            );
316
            return $stub;
317
        });
318 1
    }
319 1
320
    public function generateManyToManyTable(string $type1, string $type2): GeneratedItem
321
    {
322
        $contents = $this->stubToString('migration', function ($stub) use ($type1, $type2) {
323
            $code = <<<EOF
324 1
325 1
            \$table->increments("id");
326 1
            \$table->unsignedBigInteger("{$type1}_id");
327 1
            \$table->unsignedBigInteger("{$type2}_id");
328
EOF;
329
330 1
            $stub = str_replace(
331 1
                '// dummyCode',
332 1
                $code,
333 1
                $stub
334
            );
335
336 1
            $stub = str_replace(
337 1
                'dummytablename',
338 1
                "{$type1}_{$type2}",
339 1
                $stub
340
            );
341 1
342 1
            $stub = str_replace(
343
                'modelSchemaCode',
344 1
                '',
345 1
                $stub
346 1
            );
347 1
            return $stub;
348
        });
349 1
350
        $item = new GeneratedItem(
351
            GeneratedItem::TYPE_MIGRATION,
352 5
            $contents,
353
            $this->getGenerateFilename($type1 . '_' . $type2)
354
        );
355
        return $item;
356 5
    }
357
358
    public function getGenerateFilename(string $basename): string
359
    {
360
        $type = '_create_';
361
        
362
        /* TODO:
363
         * check if a migration '_create_'. $this->lowerName exists,
364
         * generate a diff from model(), generate new migration with diff
365
         * /
366
        $migrationFiles = \Safe\scandir($this->getBasePath('database/migrations/'));
367
        $match = '_create_' . $basename . '_table.php';
368
        foreach ($migrationFiles as $m) {
369
            if (!endsWith($m, $match)) {
370
                continue;
371
            }
372
            // get source
373
374
            // compare with this source
375
376
            // if equal ignore and don't output file
377
378
            // else generate a diff and patch
379
            $type = '_patch_';
380
        }
381
        */
382
  
383
        return $this->getBasePath(
384
            'database/migrations/' .
385
            date('Y_m_d_His') .
386
            static::$counter++ . // so we keep the same order of types in schema
387
            $type .
388
            $basename . '_table.php'
389
        );
390
    }
391
}
392