Passed
Push — master ( 6f190e...89520e )
by Bruno
09:36
created

ModelGenerator   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 571
Duplicated Lines 0 %

Test Coverage

Coverage 87.68%

Importance

Changes 19
Bugs 9 Features 1
Metric Value
eloc 327
c 19
b 9
f 1
dl 0
loc 571
ccs 242
cts 276
cp 0.8768
rs 2.4
wmc 75

9 Methods

Rating   Name   Duplication   Size   Complexity  
A generate() 0 17 1
A processField() 0 44 5
B processBasetype() 0 47 8
F processRelationship() 0 164 32
C processDirectives() 0 48 12
B generateString() 0 120 4
A setModelDir() 0 3 1
A getGenerateFilename() 0 3 2
B processGraphql() 0 27 10

How to fix   Complexity   

Complex Class

Complex classes like ModelGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelGenerator, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Laravel\Targets;
4
5
use Formularium\Datatype;
6
use Formularium\Extradata;
7
use Formularium\ExtradataParameter;
8
use Formularium\Field;
9
use Formularium\Model;
10
use Illuminate\Support\Str;
11
use GraphQL\Type\Definition\ListOfType;
12
use GraphQL\Type\Definition\NonNull;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\UnionType;
15
use Modelarium\BaseGenerator;
16
use Modelarium\Datatypes\Datatype_relationship;
17
use Modelarium\Datatypes\RelationshipFactory;
18
use Modelarium\Exception\Exception;
19
use Modelarium\FormulariumUtils;
20
use Modelarium\GeneratedCollection;
21
use Modelarium\GeneratedItem;
22
use Modelarium\Parser;
23
use Modelarium\Types\FormulariumScalarType;
24
use Nette\PhpGenerator\Method;
25
use GraphQL\Language\AST\DirectiveNode;
26
27
class ModelGenerator extends BaseGenerator
28
{
29
    /**
30
     * @var string
31
     */
32
    protected $stubDir = __DIR__ . "/stubs/";
33
34
    /**
35
     * @var string
36
     */
37
    protected static $modelDir = 'app/Models/';
38
39
    /**
40
     * @var ObjectType
41
     */
42
    protected $type = null;
43
44
    /**
45
     * @var \Nette\PhpGenerator\ClassType
46
     */
47
    protected $class = null;
48
49
    /**
50
     * fillable attributes
51
     *
52
     * @var array
53
     */
54
    protected $fillable = [];
55
56
    /**
57
     * fillable attributes
58
     *
59
     * @var array
60
     */
61
    protected $hidden = [];
62
63
    /**
64
     * cast attributes
65
     *
66
     * @var array
67
     */
68
    protected $casts = [];
69
70
    /**
71
     *
72
     * @var string
73
     */
74
    protected $parentClassName = '\Illuminate\Database\Eloquent\Model';
75
76
    /**
77
     * fields
78
     *
79
     * @var Model
80
     */
81
    protected $fModel = null;
82
83
    /**
84
     *
85 8
     * @var array
86
     */
87 8
    protected $traits = [];
88 8
89 8
    /**
90 8
     * cast attributes
91 8
     *
92
     * @var Method
93 8
     */
94 8
    protected $methodRandom = null;
95 8
96 8
    /**
97 8
     * If true, we have timestamps on the migration.
98
     *
99
     * @var boolean
100 8
     */
101
    protected $migrationTimestamps = false;
102
103 10
    public function generate(): GeneratedCollection
104
    {
105
        $this->fModel = Model::create($this->studlyName);
106
        $x = new GeneratedCollection([
107
            new GeneratedItem(
108
                GeneratedItem::TYPE_MODEL,
109 10
                $this->generateString(),
110
                $this->getGenerateFilename()
111 10
            ),
112 10
            new GeneratedItem(
113
                GeneratedItem::TYPE_MODEL,
114
                $this->templateStub('model'),
115 10
                $this->getGenerateFilename(false),
116
                true
117 10
            )
118 10
        ]);
119
        return $x;
120 8
    }
121 8
122 8
    protected function processField(
123 8
        string $typeName,
124
        \GraphQL\Type\Definition\FieldDefinition $field,
125 5
        \GraphQL\Language\AST\NodeList $directives,
126 5
        bool $isRequired
127 5
    ): void {
128 5
        $fieldName = $field->name;
129 5
130
        if ($typeName === 'ID') {
131
            return;
132
        }
133
134
        $scalarType = $this->parser->getScalarType($typeName);
135 10
136 10
        /**
137 10
         * @var Field $field
138 10
         */
139 10
        $field = null;
140
        if (!$scalarType) {
141
            // probably another model
142
            $field = FormulariumUtils::getFieldFromDirectives(
143 10
                $fieldName,
144 10
                $typeName,
145
                $directives
146 10
            );
147
        } elseif ($scalarType instanceof FormulariumScalarType) {
148
            $field = FormulariumUtils::getFieldFromDirectives(
149
                $fieldName,
150 10
                $scalarType->getDatatype()->getName(),
151
                $directives
152 10
            );
153
        } else {
154 10
            return;
155
        }
156
157
        if ($isRequired) {
158
            $field->setValidatorOption(
159
                Datatype::REQUIRED,
160
                'value',
161
                true
162
            );
163
        }
164
165
        $this->fModel->appendField($field);
166
    }
167
168
    protected function processBasetype(
169
        \GraphQL\Type\Definition\FieldDefinition $field,
170
        \GraphQL\Language\AST\NodeList $directives
171
    ): void {
172
        $fieldName = $field->name;
173
174
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
175
176
        foreach ($directives as $directive) {
177
            $name = $directive->name->value;
178
            switch ($name) {
179
            case 'modelFillable':
180 10
                $this->fillable[] = $fieldName;
181 10
                break;
182 10
            case 'modelHidden':
183
                $this->hidden[] = $fieldName;
184 8
                break;
185
186
            case 'migrationUniqueIndex':
187
                $this->class->addMethod('from' . Str::studly($fieldName))
188 8
                    ->setPublic()
189 8
                    ->setStatic()
190
                    ->setReturnType('\\App\\Models\\' . $this->studlyName)
191 8
                    ->addComment("Factory from the $fieldName unique index")
192
                    ->setBody("return {$this->studlyName}::firstWhere('$fieldName', \$value);")
193 8
                    ->addParameter('value');
194 8
                break;
195
            
196
            case 'casts':
197 8
                foreach ($directive->arguments as $arg) {
198
                    /**
199
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
200
                     */
201 8
202 8
                    $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...
203 8
204 8
                    switch ($arg->name->value) {
205
                    case 'type':
206 8
                        $this->casts[$fieldName] = $value;
207 8
                    }
208 8
                }
209 8
                break;
210 4
            }
211 4
        }
212 4
213 4
        $typeName = $type->name;
214 4
        $this->processField($typeName, $field, $directives, $isRequired);
215 4
    }
216 4
217
    protected function processRelationship(
218 8
        \GraphQL\Type\Definition\FieldDefinition $field,
219 1
        \GraphQL\Language\AST\NodeList $directives
220 1
    ): void {
221 1
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
222 1
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
223 1
224 1
        $targetClass = '\\App\\Models\\' . Str::studly($this->getInflector()->singularize($field->name));
225 1
226
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
227 7
        $typeName = $type->name;
228 3
229 3
        // special types that should be skipped.
230 3
        if ($typeName === 'Can') {
231 3
            return;
232 3
        }
233 3
234
        $generateRandom = false;
235 7
        $sourceTypeName = $this->lowerName;
236 1
        $targetTypeName = $lowerName;
237 1
        $relationship = null;
238 1
        $isInverse = false;
239 1
240 1
        foreach ($directives as $directive) {
241 1
            $name = $directive->name->value;
242 1
            switch ($name) {
243
            case 'belongsTo':
244 7
                $generateRandom = true;
245 7
                $relationship = RelationshipFactory::RELATIONSHIP_ONE_TO_MANY;
246 7
                $isInverse = true;
247 3
                $this->class->addMethod($lowerName)
248 1
                    ->setPublic()
249
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo')
250 2
                    ->setBody("return \$this->belongsTo($targetClass::class);");
251
                break;
252
253 3
            case 'belongsToMany':
254 3
                $generateRandom = true;
255
                $relationship = RelationshipFactory::RELATIONSHIP_MANY_TO_MANY;
256 3
                $isInverse = true;
257
                $this->class->addMethod($lowerNamePlural)
258
                    ->setPublic()
259 3
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany')
260 3
                    ->setBody("return \$this->belongsToMany($targetClass::class);");
261 3
                break;
262 3
263 3
            case 'hasOne':
264 3
                $relationship = RelationshipFactory::RELATIONSHIP_ONE_TO_ONE;
265
                $isInverse = false;
266
                $this->class->addMethod($lowerName)
267 3
                    ->setPublic()
268
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne')
269
                    ->setBody("return \$this->hasOne($targetClass::class);");
270
                break;
271 3
272
            case 'hasMany':
273 3
                $relationship = RelationshipFactory::RELATIONSHIP_ONE_TO_MANY;
274 3
                $isInverse = false;
275 3
                $target = $this->getInflector()->singularize($targetClass);
276
                $this->class->addMethod($lowerNamePlural)
277 7
                    ->setPublic()
278 2
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany')
279 2
                    ->setBody("return \$this->hasMany($target::class);");
280 2
                break;
281 2
282 2
            case 'morphOne':
283 2
            case 'morphMany':
284
            case 'morphToMany':
285 5
                if ($name === 'morphOne') {
286 1
                    $relationship = RelationshipFactory::MORPH_ONE_TO_ONE;
287 1
                } else {
288
                    $relationship = RelationshipFactory::MORPH_ONE_TO_MANY;
289 1
                }
290 1
                $isInverse = false;
291 1
292
                $targetType = $this->parser->getType($typeName);
293
                if (!$targetType) {
294
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->baseName}");
295
                } elseif (!($targetType instanceof ObjectType)) {
296
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->baseName}");
297
                }
298 1
                $targetField = null;
299
                foreach ($targetType->getFields() as $subField) {
300 1
                    $subDir = Parser::getDirectives($subField->astNode->directives);
301
                    if (array_key_exists('morphTo', $subDir) || array_key_exists('morphedByMany', $subDir)) {
302
                        $targetField = $subField->name;
303 1
                        break;
304 1
                    }
305
                }
306 1
                if (!$targetField) {
307 1
                    throw new Exception("{$targetType} does not have a '@morphTo' or '@morphToMany' field");
308
                }
309
310 1
                $this->class->addMethod($field->name)
311 1
                    // TODO: return type
312 1
                    ->setPublic()
313 1
                    ->setBody("return \$this->{$name}($typeName::class, '$targetField');");
314 1
                break;
315
    
316
            case 'morphTo':
317 1
                $relationship = RelationshipFactory::MORPH_ONE_TO_MANY; // TODO
318
                $isInverse = true;
319
                $this->class->addMethod($field->name)
320 4
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo')
321
                    ->setPublic()
322
                    ->setBody("return \$this->morphTo();");
323 8
                break;
324
325
            case 'morphedByMany':
326
                $relationship = RelationshipFactory::MORPH_MANY_TO_MANY; // TODO
327 8
                $isInverse = true;
328
                $typeMap = $this->parser->getSchema()->getTypeMap();
329 8
       
330
                foreach ($typeMap as $name => $object) {
331 8
                    if (!($object instanceof ObjectType) || $name === 'Query' || $name === 'Mutation' || $name === 'Subscription') {
332 5
                        continue;
333 5
                    }
334 5
335 5
                    /**
336
                     * @var ObjectType $object
337
                     */
338 8
339
                    if (str_starts_with((string)$name, '__')) {
340 10
                        // internal type
341
                        continue;
342
                    }
343 10
344 1
                    foreach ($object->getFields() as $subField) {
345 1
                        $subDirectives = Parser::getDirectives($subField->astNode->directives);
346 1
347 1
                        if (!array_key_exists('morphToMany', $subDirectives)) {
348 1
                            continue;
349 1
                        }
350
351
                        $methodName = $this->getInflector()->pluralize(mb_strtolower((string)$name));
352 1
                        $this->class->addMethod($methodName)
353
                                ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany')
354
                                ->setPublic()
355 1
                                ->setBody("return \$this->morphedByMany($name::class, '$lowerName');");
356 1
                    }
357 1
                }
358 1
                break;
359
            
360
            default:
361
                break;
362
            }
363
        }
364
        if (!$relationship) {
365
            throw new Exception("Could not find a relationship in {$typeName} for {$field->name} in {$sourceTypeName}");
366
        }
367
368
        $relationshipDatatype = "relationship:" . ($isInverse ? "inverse:" : "") .
369
            "$relationship:$sourceTypeName:$targetTypeName";
370
371
        $this->processField($relationshipDatatype, $field, $directives, $isRequired);
372
373 10
        if ($generateRandom) {
374
            if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) {
375
                // TODO: do we generate it? seed should do it?
376
            } else {
377
                $this->methodRandom->addBody(
378
                    '$data["' . $lowerName . '_id"] = function () {' . "\n" .
379
                '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
380
                '};'
381
                );
382
            }
383
        }
384
    }
385
386
    protected function processDirectives(
387
        \GraphQL\Language\AST\NodeList $directives
388
    ): void {
389
        foreach ($directives as $directive) {
390
            $name = $directive->name->value;
391
            $this->fModel->appendExtradata(FormulariumUtils::directiveToExtradata($directive));
392
393 10
            switch ($name) {
394
            case 'migrationSoftDeletes':
395 10
                $this->traits[] = '\Illuminate\Database\Eloquent\SoftDeletes';
396 10
                break;
397 10
            case 'modelNotifiable':
398 10
                $this->traits[] = '\Illuminate\Notifications\Notifiable';
399 10
                break;
400 10
            case 'modelMustVerifyEmail':
401 10
                $this->traits[] = '\Illuminate\Notifications\MustVerifyEmail';
402
                break;
403 10
            case 'migrationRememberToken':
404 10
                $this->hidden[] = 'remember_token';
405 10
                break;
406 10
            case 'migrationTimestamps':
407
                $this->migrationTimestamps = true;
408 10
                break;
409 10
            case 'modelExtends':
410 10
                foreach ($directive->arguments as $arg) {
411
                    /**
412
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
413 10
                     */
414
415 10
                    $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...
416 1
417
                    switch ($arg->name->value) {
418
                    case 'class':
419 10
                        $this->parentClassName = $value;
420 10
                    }
421 10
                }
422 10
                break;
423 10
            case 'renderable':
424
                foreach ($directive->arguments as $arg) {
425 10
                    /**
426 10
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
427 10
                     */
428 10
429 10
                    $argName = $arg->name->value;
430
                    $argValue = $arg->value->value; /** @phpstan-ignore-line */
431 10
                    $this->fModel->appendRenderable($argName, $argValue);
0 ignored issues
show
Bug introduced by
The method appendRenderable() does not exist on Formularium\Model. ( Ignorable by Annotation )

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

431
                    $this->fModel->/** @scrutinizer ignore-call */ 
432
                                   appendRenderable($argName, $argValue);

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...
432 10
                }
433 10
                break;
434 10
            }
435 10
        }
436
    }
437 10
438 10
    public function generateString(): string
439 10
    {
440 10
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
441 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
442 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
443 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
444
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo');
445 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphOne');
446
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany');
447
        $namespace->addUse('\\Illuminate\\Support\\Facades\\Auth');
448
        $namespace->addUse('\\Formularium\\Exception\\NoRandomException');
449 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
450 10
451 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
452 10
        $this->class
453 10
            ->addComment("This file was automatically generated by Modelarium.")
454 10
            ->setAbstract();
455
456 10
        $this->methodRandom = new Method('getRandomData');
457
        $this->methodRandom->addBody(
458 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
459
        );
460
461
        $this->processGraphql();
462 10
463 10
        // this might have changed
464 10
        $this->class->setExtends($this->parentClassName);
465 10
466 10
        foreach ($this->traits as $trait) {
467 10
            $this->class->addTrait($trait);
468 10
        }
469
470
        $this->class->addProperty('fillable')
471 10
            ->setProtected()
472 10
            ->setValue($this->fillable)
473 10
            ->setComment("The attributes that are mass assignable.\n@var array")
474 10
            ->setInitialized();
475 10
476 10
        $this->class->addProperty('hidden')
477 10
            ->setProtected()
478 10
            ->setValue($this->hidden)
479 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
480 10
            ->setInitialized();
481
482
        if (!$this->migrationTimestamps) {
483 10
            $this->class->addProperty('timestamps')
484 10
                ->setPublic()
485
                ->setValue(false)
486
                ->setComment("Do not set timestamps.\n@var boolean")
487 10
                ->setInitialized();
488
        }
489 10
490 10
        if ($this->casts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->casts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
491
            $this->class->addProperty('casts')
492 10
                ->setProtected()
493 10
                ->setValue($this->casts)
494 10
                ->setComment("The attributes that should be cast.\n@var array")
495 10
                ->setInitialized();
496 10
        }
497 10
498 10
        $this->class->addMethod('getFields')
499
            ->setPublic()
500
            ->setStatic()
501
            ->setReturnType('array')
502 8
            ->addComment('@return array')
503
            ->addBody(
504 10
                "return ?;\n",
505
                [
506
                    $this->fModel->serialize()
507
                ]
508
            );
509
510
        $this->class->addMethod('getFormularium')
511 10
            ->setPublic()
512 10
            ->setStatic()
513 10
            ->setReturnType('\Formularium\Model')
514
            ->addComment('@return \Formularium\Model')
515 10
            ->addBody(
516
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
517 8
                'return $model;',
518
                [
519 8
                    //$this->studlyName,
520
                ]
521
            );
522
        
523
        $this->methodRandom
524
            ->addComment('@return array')
525
            ->setPublic()
526
            ->setStatic()
527
            ->setReturnType('array')
528
            ->addBody('return $data;');
529
        $this->class->addMember($this->methodRandom);
530
531
        $this->class->addMethod('getRandomFieldData')
532
            ->setPublic()
533
            ->setStatic()
534
            ->addComment("Filters fields and generate random data. Throw NoRandomException for fields you don't want to generate random data, or return a valid value.")
535
            ->addBody('
536
$d = $f->getDatatype();
537
if ($d instanceof Datatype_relationship) {
538
    throw new NoRandomException($f->getName());
539
}
540
return $f->getDatatype()->getRandom();')
541
            ->addParameter('f')->setType('Formularium\Field');
542
543
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
544
        $this->class->addMethod('getCanAttribute')
545
            ->setPublic()
546
            ->setReturnType('array')
547
            ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return \Formularium\Model")
548
            ->addBody(
549
                '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
550
                '$user = Auth::user();' . "\n" .
551
                'return [' . "\n" .
552
                '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
553
                '];'
554
            );
555
        
556
        $printer = new \Nette\PhpGenerator\PsrPrinter;
557
        return $this->phpHeader() . $printer->printNamespace($namespace);
558
    }
559
560
    protected function processGraphql(): void
561
    {
562
        foreach ($this->type->getFields() as $field) {
563
            $directives = $field->astNode->directives;
564
            if (
565
                ($field->type instanceof ObjectType) ||
566
                ($field->type instanceof ListOfType) ||
567
                ($field->type instanceof UnionType) ||
568
                ($field->type instanceof NonNull && (
569
                    ($field->type->getWrappedType() instanceof ObjectType) ||
570
                    ($field->type->getWrappedType() instanceof ListOfType) ||
571
                    ($field->type->getWrappedType() instanceof UnionType)
572
                ))
573
            ) {
574
                // relationship
575
                $this->processRelationship($field, $directives);
576
            } else {
577
                $this->processBasetype($field, $directives);
578
            }
579
        }
580
581
        /**
582
         * @var \GraphQL\Language\AST\NodeList|null
583
         */
584
        $directives = $this->type->astNode->directives;
585
        if ($directives) {
586
            $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

586
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
587
        }
588
    }
589
590
    public function getGenerateFilename(bool $base = true): string
591
    {
592
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
593
    }
594
595
    public static function setModelDir(string $dir): void
596
    {
597
        self::$modelDir = $dir;
598
    }
599
}
600