• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RonasIT / laravel-entity-generator / 20486168500

24 Dec 2025 12:26PM UTC coverage: 99.696% (-0.3%) from 100.0%
20486168500

Pull #234

github

web-flow
Merge 8542287d3 into 681de5de6
Pull Request #234: fix: rename `createableResource` to `creatableResource`

7 of 10 new or added lines in 4 files covered. (70.0%)

1 existing line in 1 file now uncovered.

984 of 987 relevant lines covered (99.7%)

10.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.35
/src/Generators/EntityGenerator.php
1
<?php
2

3
namespace RonasIT\Support\Generators;
4

5
use Throwable;
6
use ReflectionClass;
7
use ReflectionMethod;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Str;
10
use Illuminate\Support\Facades\DB;
11
use Illuminate\Filesystem\Filesystem;
12
use RonasIT\Support\DTO\RelationsDTO;
13
use RonasIT\Support\Events\WarningEvent;
14
use Illuminate\Database\Eloquent\Relations\BelongsTo;
15
use RonasIT\Support\Exceptions\ResourceNotExistsException;
16
use RonasIT\Support\Exceptions\IncorrectClassPathException;
17
use RonasIT\Support\Exceptions\ResourceAlreadyExistsException;
18

19
/**
20
 * @property Filesystem $fs
21
 */
22
abstract class EntityGenerator
23
{
24
    const AVAILABLE_FIELDS = [
25
        'integer', 'integer-required', 'string-required', 'string', 'float-required', 'float',
26
        'boolean-required', 'boolean', 'timestamp-required', 'timestamp', 'json'
27
    ];
28

29
    const LOVER_CASE_DIRECTORIES_MAP = [
30
        'migrations' => 'database/migrations',
31
        'factories' => 'database/factories',
32
        'seeders' => 'database/seeders',
33
        'database_seeder' => 'database/seeders',
34
        'tests' => 'tests',
35
        'routes' => 'routes',
36
        'translations' => 'lang/en',
37
    ];
38

39
    protected $paths = [];
40
    protected $model;
41
    protected $modelSubFolder = '';
42
    protected $fields;
43
    protected $relations = [];
44
    protected $crudOptions;
45

46

47
    public function setCrudOptions(array $crudOptions): self
48
    {
49
        $this->crudOptions = $crudOptions;
29✔
50

51
        return $this;
29✔
52
    }
53

54
    public function setModel(string $model): self
55
    {
56
        $this->model = $model;
88✔
57

58
        return $this;
88✔
59
    }
60

61
    public function setModelSubFolder(string $folder): self
62
    {
63
        $this->modelSubFolder = $folder;
10✔
64

65
        return $this;
10✔
66
    }
67

68
    public function setFields(array $fields): self
69
    {
70
        $this->fields = $fields;
31✔
71

72
        return $this;
31✔
73
    }
74

75
    public function setRelations(RelationsDTO $relations): self
76
    {
77
        $this->relations = $relations;
33✔
78

79
        foreach ($relations->belongsTo as $field) {
33✔
80
            $relatedModel = Str::afterLast($field, '/');
14✔
81

82
            $name = Str::snake($relatedModel) . '_id';
14✔
83

84
            $this->fields['integer-required'][] = $name;
14✔
85
        }
86

87
        return $this;
33✔
88
    }
89

90
    public function __construct()
91
    {
92
        $this->paths = config('entity-generator.paths');
89✔
93

94
        $this->checkConfigHasCorrectPaths();
89✔
95
    }
96

97
    protected function generateNamespace(string $path, ?string $additionalSubFolder = null): string
98
    {
99
        $pathParts = $this->getNamespacePathParts($path, $additionalSubFolder);
45✔
100

101
        $namespace = array_map(fn (string $part) => ucfirst($part), $pathParts);
45✔
102

103
        return implode('\\', $namespace);
45✔
104
    }
105

106
    protected function createNamespace(string $configPath, ?string $subFolder = null): void
107
    {
108
        $path = $this->getPath($this->paths[$configPath], $subFolder);
51✔
109

110
        $fullPath = base_path($path);
51✔
111

112
        if (!file_exists($fullPath)) {
51✔
113
            mkdir($fullPath, 0777, true);
36✔
114
        }
115
    }
116

117
    protected function getNamespacePathParts(string $path, ?string $additionalSubFolder = null): array
118
    {
119
        $pathParts = explode('/', $this->getPath($path, $additionalSubFolder));
89✔
120

121
        if (Str::endsWith(Arr::last($pathParts), '.php')) {
89✔
122
            array_pop($pathParts);
89✔
123
        }
124

125
        return $pathParts;
89✔
126
    }
127

128
    protected function getPath(string $path, ?string $subFolder = null): string
129
    {
130
        return when($subFolder, fn () => Str::finish($path, '/') . $subFolder, $path);
89✔
131
    }
132

133
    protected function isFolderHasCorrectCase(string $folder, string $configPath): bool
134
    {
135
        $directory = Arr::get(self::LOVER_CASE_DIRECTORIES_MAP, $configPath);
89✔
136

137
        $firstFolderChar = substr($folder, 0, 1);
89✔
138

139
        return $folder === 'app' || (ucfirst($firstFolderChar) === $firstFolderChar) || Str::contains($directory, $folder);
89✔
140
    }
141

142
    abstract public function generate(): void;
143

144
    protected function classExists(string $path, string $name, ?string $subFolder = null): bool
145
    {
146
        $relativePath = $this->getClassPath($path, $name, $subFolder);
44✔
147

148
        $absolutePath = base_path($relativePath);
44✔
149

150
        return file_exists($absolutePath);
44✔
151
    }
152

153
    protected function fileExists(string $path, string $name, ?string $subFolder = null): bool
154
    {
NEW
UNCOV
155
        $relativePath = $this->getPath($this->paths[$path], $subFolder);
×
156

NEW
157
        $absolutePath = base_path("{$relativePath}/{$name}");
×
158

NEW
159
        return file_exists($absolutePath);
×
160
    }
161

162
    protected function getClassPath(string $path, string $name, ?string $subFolder = null): string
163
    {
164
        $path = $this->getPath($this->paths[$path], $subFolder);
67✔
165

166
        $fileName = str_contains($name, '.') ? $name : "{$name}.php";
67✔
167

168
        return "{$path}/{$fileName}";
67✔
169
    }
170

171
    protected function saveClass($path, $name, $content, ?string $entityFolder = null): string
172
    {
173
        $entitiesPath = base_path($this->paths[$path]);
36✔
174

175
        if (Str::endsWith($entitiesPath, '.php')) {
36✔
176
            list(, $entitiesPath) = extract_last_part($entitiesPath, '/');
1✔
177
        }
178

179
        if (!empty($entityFolder)) {
36✔
180
            $entitiesPath = "{$entitiesPath}/{$entityFolder}";
12✔
181
        }
182

183
        $classPath = "{$entitiesPath}/{$name}.php";
36✔
184
        $tag = "<?php";
36✔
185

186
        if (!Str::contains($content, $tag)) {
36✔
187
            $content = "{$tag}\n\n{$content}";
36✔
188
        }
189

190
        if (!file_exists($entitiesPath)) {
36✔
191
            mkdir($entitiesPath, 0777, true);
13✔
192
        }
193

194
        return file_put_contents($classPath, $content);
36✔
195
    }
196

197
    protected function getStub($stub, $data = []): string
198
    {
199
        $stubPath = config("entity-generator.stubs.{$stub}");
45✔
200

201
        $data['options'] = $this->crudOptions;
45✔
202

203
        return view($stubPath)->with($data)->render();
45✔
204
    }
205

206
    protected function getTableName($entityName, $delimiter = '_'): string
207
    {
208
        $entityName = Str::snake($entityName, $delimiter);
21✔
209

210
        return $this->getPluralName($entityName);
21✔
211
    }
212

213
    protected function getPluralName($entityName): string
214
    {
215
        return Str::plural($entityName);
27✔
216
    }
217

218
    protected function throwFailureException($exceptionClass, $failureMessage, $recommendedMessage): void
219
    {
220
        throw new $exceptionClass("{$failureMessage} {$recommendedMessage}");
5✔
221
    }
222

223
    protected function getRelatedModels(string $model, string $creatableClass): array
224
    {
225
        $modelClass = $this->getModelClass($model);
14✔
226

227
        if (!class_exists($modelClass)) {
14✔
228
            throw new ResourceNotExistsException($creatableClass, $model);
2✔
229
        }
230

231
        $instance = new $modelClass();
12✔
232

233
        $publicMethods = (new ReflectionClass($modelClass))->getMethods(ReflectionMethod::IS_PUBLIC);
12✔
234

235
        $methods = array_filter($publicMethods, fn ($method) => $method->class === $modelClass && !$method->getParameters());
12✔
236

237
        $relatedModels = [];
12✔
238

239
        DB::beginTransaction();
12✔
240

241
        foreach ($methods as $method) {
12✔
242
            try {
243
                $result = call_user_func([$instance, $method->getName()]);
12✔
244

245
                if (!$result instanceof BelongsTo) {
12✔
246
                    continue;
12✔
247
                }
248
            } catch (Throwable) {
12✔
249
                continue;
12✔
250
            }
251

252
            $relatedModels[] = $this->generateRelativePath(get_class($result->getRelated()), $this->paths['models']);
8✔
253
        }
254

255
        DB::rollBack();
12✔
256

257
        return $relatedModels;
12✔
258
    }
259

260
    protected function generateRelativePath(string $namespace, string $basePath): string
261
    {
262
        return Str::after(
8✔
263
            subject: $this->namespaceToPath($namespace),
8✔
264
            search: $this->namespaceToPath($basePath) . '/',
8✔
265
        );
8✔
266
    }
267

268
    protected function namespaceToPath(string $namespace): string
269
    {
270
        return str_replace('\\', '/', $namespace);
8✔
271
    }
272

273
    protected function getModelClass(string $model): string
274
    {
275
        $subfolder = when($model === $this->model, $this->modelSubFolder);
21✔
276

277
        $modelNamespace = $this->generateNamespace($this->paths['models'], $subfolder);
21✔
278

279
        return "{$modelNamespace}\\{$model}";
21✔
280
    }
281

282
    protected function isStubExists(string $stubName, ?string $generationType = null): bool
283
    {
284
        $config = "entity-generator.stubs.{$stubName}";
68✔
285

286
        $stubPath = config($config);
68✔
287

288
        if (!view()->exists($stubPath)) {
68✔
289
            $generationType ??= Str::replace('_', ' ', $stubName);
20✔
290

291
            $message = "Generation of {$generationType} has been skipped cause the view {$stubPath} from the config {$config} is not exists. Please check that config has the correct view name value.";
20✔
292

293
            event(new WarningEvent($message));
20✔
294

295
            return false;
20✔
296
        }
297

298
        return true;
57✔
299
    }
300

301
    protected function prepareRelations(): array
302
    {
303
        $result = [];
12✔
304

305
        foreach ($this->relations as $relationType => $relations) {
12✔
306
            $result[$relationType] = array_map(fn ($relation) => class_basename($relation), $relations);
12✔
307
        }
308

309
        return $result;
12✔
310
    }
311

312
    protected function pathToNamespace(string $name): string
313
    {
314
        return ucwords(Str::replace('/', '\\', $name), '\\');
5✔
315
    }
316

317
    protected function checkConfigHasCorrectPaths(): void
318
    {
319
        foreach ($this->paths as $configPath => $path) {
89✔
320
            $pathParts = $this->getNamespacePathParts($path);
89✔
321

322
            foreach ($pathParts as $part) {
89✔
323
                if (!$this->isFolderHasCorrectCase($part, $configPath)) {
89✔
324
                    throw new IncorrectClassPathException("Incorrect path to {$configPath}, {$part} folder must start with a capital letter, please specify the path according to the PSR.");
1✔
325
                }
326
            }
327
        }
328
    }
329

330
    protected function checkResourceExists(string $path, string $resourceName, ?string $subFolder = null): void
331
    {
332
        if ($this->classExists($path, $resourceName, $subFolder)) {
70✔
333
            $filePath = $this->getClassPath($path, $resourceName, $subFolder);
20✔
334

335
            throw new ResourceAlreadyExistsException($filePath);
20✔
336
        }
337
    }
338

339
    protected function checkResourceNotExists(string $path, string $creatableResource, string $requiredResource, ?string $subFolder = null): void
340
    {
341
        if (!$this->classExists($path, $requiredResource, $subFolder)) {
36✔
342
            $filePath = $this->getClassPath($path, $requiredResource, $subFolder);
6✔
343

344
            throw new ResourceNotExistsException($creatableResource, $filePath);
6✔
345
        }
346
    }
347

348
    protected function getRelationName(string $relation, string $type): string
349
    {
350
        $relationName = Str::snake($relation);
8✔
351

352
        if ($this->isPluralRelation($type)) {
8✔
353
            $relationName = Str::plural($relationName);
7✔
354
        }
355

356
        return $relationName;
8✔
357
    }
358

359
    protected function isPluralRelation(string $relation): bool
360
    {
361
        return in_array($relation, ['hasMany', 'belongsToMany']);
8✔
362
    }
363
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc