Passed
Push — main ( 073c01...b49c3d )
by Dimitri
03:21
created

GeneratorTrait::formatFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Cli\Traits;
13
14
use BlitzPHP\Container\Services;
15
use BlitzPHP\View\Adapters\NativeAdapter;
16
17
/**
18
 * GeneratorTrait contient une collection de méthodes
19
 * pour construire les commandes qui génèrent un fichier.
20
 */
21
trait GeneratorTrait
22
{
23
    /**
24
     * Nom du composant
25
     *
26
     * @var string
27
     */
28
    protected $component;
29
30
    /**
31
     * Répertoire de fichiers
32
     *
33
     * @var string
34
     */
35
    protected $directory;
36
37
    /**
38
     * Nom de la vue du template
39
     *
40
     * @var string
41
     */
42
    protected $template;
43
44
    /**
45
     * Chemin dans du dossier dans lequelle les vues de generation sont cherchees
46
     *
47
     * @var string
48
     */
49
    protected $templatePath = SYST_PATH . 'Cli/Commands/Generators/Views';
50
51
    /**
52
     * Clé de chaîne de langue pour les noms de classe requis.
53
     *
54
     * @var string
55
     */
56
    protected $classNameLang = '';
57
58
    /**
59
     * S'il faut exiger le nom de la classe.
60
     *
61
     * @internal
62
     *
63
     * @var bool
64
     */
65
    private $hasClassName = true;
66
67
    /**
68
     * S'il faut trier les importations de classe.
69
     *
70
     * @internal
71
     *
72
     * @var bool
73
     */
74
    private $sortImports = true;
75
76
    /**
77
     * Indique si l'option `--suffix` a un effet.
78
     *
79
     * @internal
80
     *
81
     * @var bool
82
     */
83
    private $enabledSuffixing = true;
84
85
    /**
86
     * Le tableau params pour un accès facile par d'autres méthodes.
87
     *
88
     * @internal
89
     *
90
     * @var array
91
     */
92
    private $params = [];
93
94
    /**
95
     * Exécute la commande.
96
     */
97
    protected function runGeneration(array $params): void
98
    {
99
        $this->params = $params;
100
101
        // Recupere le FQCN.
102
        $class = $this->qualifyClassName();
103
104
        // Recupere le chemin du fichier a partir du nom de la classe.
105
        $target = $this->buildPath($class);
106
107
        if (empty($target)) {
108
            return;
109
        }
110
111
        $this->generateFile($target, $this->buildContent($class));
112
    }
113
114
    /**
115
     * Handles writing the file to disk, and all of the safety checks around that.
116
     */
117
    private function generateFile(string $target, string $content): void
118
    {
119
        if ($this->option('namespace') === 'BlitzPHP') {
0 ignored issues
show
Bug introduced by
It seems like option() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

119
        if ($this->/** @scrutinizer ignore-call */ option('namespace') === 'BlitzPHP') {
Loading history...
120
            // @codeCoverageIgnoreStart
121
            $this->colorize(lang('CLI.generator.usingBlitzNamespace'), 'yellow');
0 ignored issues
show
Bug introduced by
It seems like colorize() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

121
            $this->/** @scrutinizer ignore-call */ 
122
                   colorize(lang('CLI.generator.usingBlitzNamespace'), 'yellow');
Loading history...
122
123
            if (! $this->confirm('Are you sure you want to continue?')) {
0 ignored issues
show
Bug introduced by
It seems like confirm() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

123
            if (! $this->/** @scrutinizer ignore-call */ confirm('Are you sure you want to continue?')) {
Loading history...
124
                $this->eol()->colorize(lang('CLI.generator.cancelOperation'), 'yellow');
0 ignored issues
show
Bug introduced by
It seems like eol() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

124
                $this->/** @scrutinizer ignore-call */ 
125
                       eol()->colorize(lang('CLI.generator.cancelOperation'), 'yellow');
Loading history...
125
126
                return;
127
            }
128
129
            $this->eol();
130
            // @codeCoverageIgnoreEnd
131
        }
132
133
        $isFile = is_file($target);
134
135
        // Écraser des fichiers sans le savoir est une gêne sérieuse, nous allons donc vérifier si nous dupliquons des choses,
136
        // si l'option "forcer" n'est pas fournie, nous renvoyons.
137
        if (! $this->option('force') && $isFile) {
138
            $this->io->error(lang('CLI.generator.fileExist', [clean_path($target)]), true);
139
140
            return;
141
        }
142
143
        // Vérifie si le répertoire pour enregistrer le fichier existe.
144
        $dir = dirname($target);
145
146
        if (! is_dir($dir)) {
147
            mkdir($dir, 0755, true);
148
        }
149
150
        helper('filesystem');
151
152
        // Construisez la classe en fonction des détails dont nous disposons.
153
        // Nous obtiendrons le contenu de notre fichier à partir du modèle,
154
        // puis nous effectuerons les remplacements nécessaires.
155
        if (! write_file($target, $content)) {
156
            // @codeCoverageIgnoreStart
157
            $this->io->error(lang('CLI.generator.fileError', [clean_path($target)]), true);
158
159
            return;
160
            // @codeCoverageIgnoreEnd
161
        }
162
163
        if ($this->option('force') && $isFile) {
164
            $this->colorize(lang('CLI.generator.fileOverwrite', [clean_path($target)]), 'yellow');
165
166
            return;
167
        }
168
169
        $this->colorize(lang('CLI.generator.fileCreate', [clean_path($target)]), 'green');
170
    }
171
172
    /**
173
     * Préparez les options et effectuez les remplacements nécessaires.
174
     */
175
    protected function prepare(string $class): string
176
    {
177
        return $this->parseTemplate($class);
178
    }
179
180
    /**
181
     * Changez le nom de base du fichier avant de l'enregistrer.
182
     *
183
     * Utile pour les composants dont le nom de fichier comporte une date.
184
     */
185
    protected function basename(string $filename): string
186
    {
187
        return basename($filename);
188
    }
189
190
    /**
191
     * Formatte le nom de base du fichier avant de l'enregistrer.
192
     *
193
     * Utile pour les composants dont le nom de fichier doit etre dans un format particulier.
194
     */
195
196
    protected function formatFilename(string $namespace, string $class): string
197
    {
198
        return str_replace('\\', DS, trim(str_replace($namespace . '\\', '', $class), '\\'));
199
    }
200
201
    /**
202
     * Analyse le nom de la classe et vérifie s'il est déjà qualifié.
203
     */
204
    protected function qualifyClassName(): string
205
    {
206
        // Obtient le nom de la classe à partir de l'entrée.
207
        $class = $this->params[0] ?? $this->params['name'] ?? null;
208
209
        if ($class === null && $this->hasClassName) {
210
            // @codeCoverageIgnoreStart
211
            $nameLang = $this->classNameLang ?: 'CLI.generator.className.default';
212
            $class    = $this->prompt(lang($nameLang));
0 ignored issues
show
Bug introduced by
It seems like prompt() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

212
            /** @scrutinizer ignore-call */ 
213
            $class    = $this->prompt(lang($nameLang));
Loading history...
213
            $this->eol();
214
            // @codeCoverageIgnoreEnd
215
        }
216
217
        helper('inflector');
218
219
        $component = singular($this->component);
220
221
        /**
222
         * @see https://regex101.com/r/a5KNCR/1
223
         */
224
        $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)/i', $component);
225
226
        if (preg_match($pattern, $class, $matches) === 1) {
227
            $class = $matches[1] . ucfirst($matches[2]);
228
        }
229
230
        if ($this->enabledSuffixing && $this->getOption('suffix') && ! strripos($class, $component)) {
0 ignored issues
show
Bug introduced by
It seems like getOption() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

230
        if ($this->enabledSuffixing && $this->/** @scrutinizer ignore-call */ getOption('suffix') && ! strripos($class, $component)) {
Loading history...
231
            $class .= ucfirst($component);
232
        }
233
234
        // Coupe l'entrée, normalise les séparateurs et s'assure que tous les chemins sont en Pascalcase.
235
        $class = ltrim(implode('\\', array_map('pascalize', explode('\\', str_replace('/', '\\', trim($class))))), '\\/');
236
237
        // Obtient l'espace de noms à partir de l'entrée. N'oubliez pas la barre oblique inverse finale !
238
        $namespace = trim(str_replace('/', '\\', $this->getOption('namespace', APP_NAMESPACE)), '\\') . '\\';
239
240
        if (strncmp($class, $namespace, strlen($namespace)) === 0) {
241
            return $class; // @codeCoverageIgnore
242
        }
243
244
        return $namespace . $this->directory . '\\' . str_replace('/', '\\', $class);
245
    }
246
247
    /**
248
     * Obtient la vue du générateur
249
     */
250
    protected function renderTemplate(array $data = []): string
251
    {
252
        $viewer = new NativeAdapter([], $this->templatePath, false);
253
254
        return $viewer->setData($data)->render($this->template);
255
    }
256
257
    /**
258
     * Exécute les pseudo-variables contenues dans le fichier de vue.
259
     */
260
    protected function parseTemplate(string $class, array $search = [], array $replace = [], array $data = []): string
261
    {
262
        // Récupère la partie de l'espace de noms à partir du nom de classe complet.
263
        $namespace = trim(implode('\\', array_slice(explode('\\', $class), 0, -1)), '\\');
264
        $search[]  = '<@php';
265
        $search[]  = '{namespace}';
266
        $search[]  = '{class}';
267
        $replace[] = '<?php';
268
        $replace[] = $namespace;
269
        $replace[] = str_replace($namespace . '\\', '', $class);
270
271
        return str_replace($search, $replace, $this->renderTemplate($data));
272
    }
273
274
    /**
275
     * Construit le contenu de la classe générée, effectue tous les remplacements nécessaires et
276
     * trie par ordre alphabétique les importations pour un modèle donné.
277
     */
278
    protected function buildContent(string $class): string
279
    {
280
        $template = $this->prepare($class);
281
282
        if ($this->sortImports && preg_match('/(?P<imports>(?:^use [^;]+;$\n?)+)/m', $template, $match)) {
283
            $imports = explode("\n", trim($match['imports']));
284
            sort($imports);
285
286
            return str_replace(trim($match['imports']), implode("\n", $imports), $template);
287
        }
288
289
        return $template;
290
    }
291
292
    /**
293
     * Construit le chemin du fichier à partir du nom de la classe.
294
     */
295
    protected function buildPath(string $class): string
296
    {
297
        $namespace = trim(str_replace('/', '\\', $this->option('namespace', APP_NAMESPACE)), '\\');
298
299
        $base = Services::autoloader()->getNamespace($namespace);
300
301
        if (! $base = reset($base)) {
302
            $this->io->error(lang('CLI.namespaceNotDefined', [$namespace]), true);
303
304
            return '';
305
        }
306
307
        $base = realpath($base) ?: $base;
308
        $file = $base . DS . $this->formatFilename($namespace, $class) . '.php';
309
310
        return implode(DS, array_slice(explode(DS, $file), 0, -1)) . DS . $this->basename($file);
311
    }
312
313
    /**
314
     * Permet aux générateurs enfants de modifier le drapeau interne `$hasClassName`.
315
     */
316
    protected function setHasClassName(bool $hasClassName): self
317
    {
318
        $this->hasClassName = $hasClassName;
319
320
        return $this;
321
    }
322
323
    /**
324
     * Permet aux générateurs enfants de modifier le drapeau interne `$sortImports`.
325
     */
326
    protected function setSortImports(bool $sortImports): self
327
    {
328
        $this->sortImports = $sortImports;
329
330
        return $this;
331
    }
332
333
    /**
334
     * Permet aux générateurs enfants de modifier le drapeau interne `$enabledSuffixing`.
335
     */
336
    protected function setEnabledSuffixing(bool $enabledSuffixing): self
337
    {
338
        $this->enabledSuffixing = $enabledSuffixing;
339
340
        return $this;
341
    }
342
}
343