12
12
namespace Symfony \Bridge \Twig \Command ;
13
13
14
14
use Symfony \Component \Console \Command \Command ;
15
+ use Symfony \Component \Console \Exception \InvalidArgumentException ;
15
16
use Symfony \Component \Console \Input \InputArgument ;
16
17
use Symfony \Component \Console \Input \InputInterface ;
17
18
use Symfony \Component \Console \Input \InputOption ;
18
19
use Symfony \Component \Console \Output \OutputInterface ;
19
20
use Symfony \Component \Console \Style \SymfonyStyle ;
21
+ use Symfony \Component \Finder \Finder ;
20
22
use Twig \Environment ;
21
23
use Twig \Loader \FilesystemLoader ;
22
24
@@ -50,19 +52,24 @@ protected function configure()
50
52
{
51
53
$ this
52
54
->setDefinition (array (
53
- new InputArgument ('filter ' , InputArgument::OPTIONAL , 'Show details for all entries matching this filter ' ),
55
+ new InputArgument ('name ' , InputArgument::OPTIONAL , 'The template name ' ),
56
+ new InputOption ('filter ' , null , InputOption::VALUE_REQUIRED , 'Show details for all entries matching this filter ' ),
54
57
new InputOption ('format ' , null , InputOption::VALUE_REQUIRED , 'The output format (text or json) ' , 'text ' ),
55
58
))
56
59
->setDescription ('Shows a list of twig functions, filters, globals and tests ' )
57
60
->setHelp (<<<'EOF'
58
61
The <info>%command.name%</info> command outputs a list of twig functions,
59
- filters, globals and tests. Output can be filtered with an optional argument.
62
+ filters, globals and tests.
60
63
61
64
<info>php %command.full_name%</info>
62
65
63
66
The command lists all functions, filters, etc.
64
67
65
- <info>php %command.full_name% date</info>
68
+ <info>php %command.full_name% @Twig/Exception/error.html.twig</info>
69
+
70
+ The command lists all paths that match the given template name.
71
+
72
+ <info>php %command.full_name% --filter=date</info>
66
73
67
74
The command lists everything that contains the word date.
68
75
@@ -77,28 +84,107 @@ protected function configure()
77
84
protected function execute (InputInterface $ input , OutputInterface $ output )
78
85
{
79
86
$ io = new SymfonyStyle ($ input , $ output );
80
- $ types = array ('functions ' , 'filters ' , 'tests ' , 'globals ' );
87
+ $ name = $ input ->getArgument ('name ' );
88
+ $ filter = $ input ->getOption ('filter ' );
81
89
82
- if ('json ' === $ input ->getOption ('format ' )) {
83
- $ data = array ();
84
- foreach ($ types as $ type ) {
85
- foreach ($ this ->twig ->{'get ' .ucfirst ($ type )}() as $ name => $ entity ) {
86
- $ data [$ type ][$ name ] = $ this ->getMetadata ($ type , $ entity );
90
+ if (null !== $ name && !$ this ->twig ->getLoader () instanceof FilesystemLoader) {
91
+ throw new InvalidArgumentException (sprintf ('Argument "name" not supported, it requires the Twig loader "%s" ' , FilesystemLoader::class));
92
+ }
93
+
94
+ switch ($ input ->getOption ('format ' )) {
95
+ case 'text ' :
96
+ return $ name ? $ this ->displayPathsText ($ io , $ name ) : $ this ->displayGeneralText ($ io , $ filter );
97
+ case 'json ' :
98
+ return $ name ? $ this ->displayPathsJson ($ io , $ name ) : $ this ->displayGeneralJson ($ io , $ filter );
99
+ default :
100
+ throw new InvalidArgumentException (sprintf ('The format "%s" is not supported. ' , $ input ->getOption ('format ' )));
101
+ }
102
+ }
103
+
104
+ private function displayPathsText (SymfonyStyle $ io , string $ name )
105
+ {
106
+ $ files = $ this ->findTemplateFiles ($ name );
107
+ $ paths = $ this ->getLoaderPaths ($ name );
108
+
109
+ $ io ->section ('Matched File ' );
110
+ if ($ files ) {
111
+ $ io ->success (array_shift ($ files ));
112
+
113
+ if ($ files ) {
114
+ $ io ->section ('Overridden Files ' );
115
+ $ io ->listing ($ files );
116
+ }
117
+ } else {
118
+ $ alternatives = array ();
119
+
120
+ if ($ paths ) {
121
+ $ shortnames = array ();
122
+ $ dirs = array ();
123
+ foreach (current ($ paths ) as $ path ) {
124
+ $ dirs [] = $ this ->isAbsolutePath ($ path ) ? $ path : $ this ->projectDir .'/ ' .$ path ;
125
+ }
126
+ foreach (Finder::create ()->files ()->followLinks ()->in ($ dirs ) as $ file ) {
127
+ $ shortnames [] = str_replace ('\\' , '/ ' , $ file ->getRelativePathname ());
128
+ }
129
+
130
+ list ($ namespace , $ shortname ) = $ this ->parseTemplateName ($ name );
131
+ $ alternatives = $ this ->findAlternatives ($ shortname , $ shortnames );
132
+ if (FilesystemLoader::MAIN_NAMESPACE !== $ namespace ) {
133
+ $ alternatives = array_map (function ($ shortname ) use ($ namespace ) {
134
+ return '@ ' .$ namespace .'/ ' .$ shortname ;
135
+ }, $ alternatives );
87
136
}
88
137
}
89
- $ data ['tests ' ] = array_keys ($ data ['tests ' ]);
90
- $ data ['loader_paths ' ] = $ this ->getLoaderPaths ();
91
- if ($ wrongBundles = $ this ->findWrongBundleOverrides ()) {
92
- $ data ['warnings ' ] = $ this ->buildWarningMessages ($ wrongBundles );
138
+
139
+ $ this ->error ($ io , sprintf ('Template name "%s" not found ' , $ name ), $ alternatives );
140
+ }
141
+
142
+ $ io ->section ('Configured Paths ' );
143
+ if ($ paths ) {
144
+ $ io ->table (array ('Namespace ' , 'Paths ' ), $ this ->buildTableRows ($ paths ));
145
+ } else {
146
+ $ alternatives = array ();
147
+ $ namespace = $ this ->parseTemplateName ($ name )[0 ];
148
+
149
+ if (FilesystemLoader::MAIN_NAMESPACE === $ namespace ) {
150
+ $ message = 'No template paths configured for your application ' ;
151
+ } else {
152
+ $ message = sprintf ('No template paths configured for "@%s" namespace ' , $ namespace );
153
+ $ namespaces = $ this ->twig ->getLoader ()->getNamespaces ();
154
+ foreach ($ this ->findAlternatives ($ namespace , $ namespaces ) as $ namespace ) {
155
+ $ alternatives [] = '@ ' .$ namespace ;
156
+ }
93
157
}
94
158
95
- $ io -> writeln ( json_encode ( $ data ) );
159
+ $ this -> error ( $ io , $ message , $ alternatives );
96
160
97
- return 0 ;
161
+ if (!$ alternatives && $ paths = $ this ->getLoaderPaths ()) {
162
+ $ io ->table (array ('Namespace ' , 'Paths ' ), $ this ->buildTableRows ($ paths ));
163
+ }
98
164
}
165
+ }
99
166
100
- $ filter = $ input ->getArgument ('filter ' );
167
+ private function displayPathsJson (SymfonyStyle $ io , string $ name )
168
+ {
169
+ $ files = $ this ->findTemplateFiles ($ name );
170
+ $ paths = $ this ->getLoaderPaths ($ name );
171
+
172
+ if ($ files ) {
173
+ $ data ['matched_file ' ] = array_shift ($ files );
174
+ if ($ files ) {
175
+ $ data ['overridden_files ' ] = $ files ;
176
+ }
177
+ } else {
178
+ $ data ['matched_file ' ] = sprintf ('Template name "%s" not found ' , $ name );
179
+ }
180
+ $ data ['loader_paths ' ] = $ paths ;
101
181
182
+ $ io ->writeln (json_encode ($ data ));
183
+ }
184
+
185
+ private function displayGeneralText (SymfonyStyle $ io , string $ filter = null )
186
+ {
187
+ $ types = array ('functions ' , 'filters ' , 'tests ' , 'globals ' );
102
188
foreach ($ types as $ index => $ type ) {
103
189
$ items = array ();
104
190
foreach ($ this ->twig ->{'get ' .ucfirst ($ type )}() as $ name => $ entity ) {
@@ -117,46 +203,56 @@ protected function execute(InputInterface $input, OutputInterface $output)
117
203
$ io ->listing ($ items );
118
204
}
119
205
120
- $ rows = array ();
121
- $ firstNamespace = true ;
122
- $ prevHasSeparator = false ;
123
- foreach ($ this ->getLoaderPaths () as $ namespace => $ paths ) {
124
- if (!$ firstNamespace && !$ prevHasSeparator && \count ($ paths ) > 1 ) {
125
- $ rows [] = array ('' , '' );
126
- }
127
- $ firstNamespace = false ;
128
- foreach ($ paths as $ path ) {
129
- $ rows [] = array ($ namespace , $ path .\DIRECTORY_SEPARATOR );
130
- $ namespace = '' ;
206
+ if (!$ filter && $ paths = $ this ->getLoaderPaths ()) {
207
+ $ io ->section ('Loader Paths ' );
208
+ $ io ->table (array ('Namespace ' , 'Paths ' ), $ this ->buildTableRows ($ paths ));
209
+ }
210
+
211
+ if ($ wronBundles = $ this ->findWrongBundleOverrides ()) {
212
+ foreach ($ this ->buildWarningMessages ($ wronBundles ) as $ message ) {
213
+ $ io ->warning ($ message );
131
214
}
132
- if (\count ($ paths ) > 1 ) {
133
- $ rows [] = array ('' , '' );
134
- $ prevHasSeparator = true ;
135
- } else {
136
- $ prevHasSeparator = false ;
215
+ }
216
+ }
217
+
218
+ private function displayGeneralJson (SymfonyStyle $ io , $ filter )
219
+ {
220
+ $ types = array ('functions ' , 'filters ' , 'tests ' , 'globals ' );
221
+ $ data = array ();
222
+ foreach ($ types as $ type ) {
223
+ foreach ($ this ->twig ->{'get ' .ucfirst ($ type )}() as $ name => $ entity ) {
224
+ if (!$ filter || false !== strpos ($ name , $ filter )) {
225
+ $ data [$ type ][$ name ] = $ this ->getMetadata ($ type , $ entity );
226
+ }
137
227
}
138
228
}
139
- if ($ prevHasSeparator ) {
140
- array_pop ($ rows );
229
+ if (isset ($ data ['tests ' ])) {
230
+ $ data ['tests ' ] = array_keys ($ data ['tests ' ]);
231
+ }
232
+
233
+ if (!$ filter && $ paths = $ this ->getLoaderPaths ($ filter )) {
234
+ $ data ['loader_paths ' ] = $ paths ;
141
235
}
142
- $ io ->section ('Loader Paths ' );
143
- $ io ->table (array ('Namespace ' , 'Paths ' ), $ rows );
144
- $ messages = $ this ->buildWarningMessages ($ this ->findWrongBundleOverrides ());
145
- foreach ($ messages as $ message ) {
146
- $ io ->warning ($ message );
236
+
237
+ if ($ wronBundles = $ this ->findWrongBundleOverrides ()) {
238
+ $ data ['warnings ' ] = $ this ->buildWarningMessages ($ wronBundles );
147
239
}
148
240
149
- return 0 ;
241
+ $ io -> writeln ( json_encode ( $ data )) ;
150
242
}
151
243
152
- private function getLoaderPaths ()
244
+ private function getLoaderPaths (string $ name = null ): array
153
245
{
154
- if (!($ loader = $ this ->twig ->getLoader ()) instanceof FilesystemLoader) {
155
- return array ();
246
+ /** @var FilesystemLoader $loader */
247
+ $ loader = $ this ->twig ->getLoader ();
248
+ $ loaderPaths = array ();
249
+ $ namespaces = $ loader ->getNamespaces ();
250
+ if (null !== $ name ) {
251
+ $ namespace = $ this ->parseTemplateName ($ name )[0 ];
252
+ $ namespaces = array_intersect (array ($ namespace ), $ namespaces );
156
253
}
157
254
158
- $ loaderPaths = array ();
159
- foreach ($ loader ->getNamespaces () as $ namespace ) {
255
+ foreach ($ namespaces as $ namespace ) {
160
256
$ paths = array_map (function ($ path ) {
161
257
if (null !== $ this ->projectDir && 0 === strpos ($ path , $ this ->projectDir )) {
162
258
$ path = ltrim (substr ($ path , \strlen ($ this ->projectDir )), \DIRECTORY_SEPARATOR );
@@ -345,4 +441,119 @@ private function buildWarningMessages(array $wrongBundles): array
345
441
346
442
return $ messages ;
347
443
}
444
+
445
+ private function error (SymfonyStyle $ io , string $ message , array $ alternatives = array ()): void
446
+ {
447
+ if ($ alternatives ) {
448
+ if (1 === \count ($ alternatives )) {
449
+ $ message .= "\n\nDid you mean this? \n " ;
450
+ } else {
451
+ $ message .= "\n\nDid you mean one of these? \n " ;
452
+ }
453
+ $ message .= implode ("\n " , $ alternatives );
454
+ }
455
+
456
+ $ io ->block ($ message , null , 'fg=white;bg=red ' , ' ' , true );
457
+ }
458
+
459
+ private function findTemplateFiles (string $ name ): array
460
+ {
461
+ /** @var FilesystemLoader $loader */
462
+ $ loader = $ this ->twig ->getLoader ();
463
+ $ files = array ();
464
+ list ($ namespace , $ shortname ) = $ this ->parseTemplateName ($ name );
465
+
466
+ foreach ($ loader ->getPaths ($ namespace ) as $ path ) {
467
+ if (!$ this ->isAbsolutePath ($ path )) {
468
+ $ path = $ this ->projectDir .'/ ' .$ path ;
469
+ }
470
+ $ filename = $ path .'/ ' .$ shortname ;
471
+
472
+ if (is_file ($ filename )) {
473
+ if (false !== $ realpath = realpath ($ filename )) {
474
+ $ files [] = $ this ->getRelativePath ($ realpath );
475
+ } else {
476
+ $ files [] = $ this ->getRelativePath ($ filename );
477
+ }
478
+ }
479
+ }
480
+
481
+ return $ files ;
482
+ }
483
+
484
+ private function parseTemplateName (string $ name , string $ default = FilesystemLoader::MAIN_NAMESPACE ): array
485
+ {
486
+ if (isset ($ name [0 ]) && '@ ' === $ name [0 ]) {
487
+ if (false === ($ pos = strpos ($ name , '/ ' )) || $ pos === \strlen ($ name ) - 1 ) {
488
+ throw new InvalidArgumentException (sprintf ('Malformed namespaced template name "%s" (expecting "@namespace/template_name"). ' , $ name ));
489
+ }
490
+
491
+ $ namespace = substr ($ name , 1 , $ pos - 1 );
492
+ $ shortname = substr ($ name , $ pos + 1 );
493
+
494
+ return array ($ namespace , $ shortname );
495
+ }
496
+
497
+ return array ($ default , $ name );
498
+ }
499
+
500
+ private function buildTableRows (array $ loaderPaths ): array
501
+ {
502
+ $ rows = array ();
503
+ $ firstNamespace = true ;
504
+ $ prevHasSeparator = false ;
505
+
506
+ foreach ($ loaderPaths as $ namespace => $ paths ) {
507
+ if (!$ firstNamespace && !$ prevHasSeparator && \count ($ paths ) > 1 ) {
508
+ $ rows [] = array ('' , '' );
509
+ }
510
+ $ firstNamespace = false ;
511
+ foreach ($ paths as $ path ) {
512
+ $ rows [] = array ($ namespace , $ path .\DIRECTORY_SEPARATOR );
513
+ $ namespace = '' ;
514
+ }
515
+ if (\count ($ paths ) > 1 ) {
516
+ $ rows [] = array ('' , '' );
517
+ $ prevHasSeparator = true ;
518
+ } else {
519
+ $ prevHasSeparator = false ;
520
+ }
521
+ }
522
+ if ($ prevHasSeparator ) {
523
+ array_pop ($ rows );
524
+ }
525
+
526
+ return $ rows ;
527
+ }
528
+
529
+ private function findAlternatives (string $ name , array $ collection ): array
530
+ {
531
+ $ alternatives = array ();
532
+ foreach ($ collection as $ item ) {
533
+ $ lev = levenshtein ($ name , $ item );
534
+ if ($ lev <= \strlen ($ name ) / 3 || false !== strpos ($ item , $ name )) {
535
+ $ alternatives [$ item ] = isset ($ alternatives [$ item ]) ? $ alternatives [$ item ] - $ lev : $ lev ;
536
+ }
537
+ }
538
+
539
+ $ threshold = 1e3 ;
540
+ $ alternatives = array_filter ($ alternatives , function ($ lev ) use ($ threshold ) { return $ lev < 2 * $ threshold ; });
541
+ ksort ($ alternatives , SORT_NATURAL | SORT_FLAG_CASE );
542
+
543
+ return array_keys ($ alternatives );
544
+ }
545
+
546
+ private function getRelativePath (string $ path ): string
547
+ {
548
+ if (null !== $ this ->projectDir && 0 === strpos ($ path , $ this ->projectDir )) {
549
+ return ltrim (substr ($ path , \strlen ($ this ->projectDir )), \DIRECTORY_SEPARATOR );
550
+ }
551
+
552
+ return $ path ;
553
+ }
554
+
555
+ private function isAbsolutePath (string $ file ): bool
556
+ {
557
+ return strspn ($ file , '/ \\' , 0 , 1 ) || (\strlen ($ file ) > 3 && ctype_alpha ($ file [0 ]) && ': ' === $ file [1 ] && strspn ($ file , '/ \\' , 2 , 1 )) || null !== parse_url ($ file , PHP_URL_SCHEME );
558
+ }
348
559
}
0 commit comments