22
33namespace IxDFCodingStandard \Sniffs \Laravel ;
44
5- use BadMethodCallException ;
65use PHP_CodeSniffer \Files \File ;
76use PHP_CodeSniffer \Sniffs \Sniff ;
87
98final class NonExistingBladeTemplateSniff implements Sniff
109{
1110 private const INVALID_METHOD_CALL = 'Invalid method call ' ;
1211
12+ public const CODE_TEMPLATE_NOT_FOUND = 'TemplateNotFound ' ;
13+ public const CODE_UNKNOWN_VIEW_NAMESPACE = 'UnknownViewNamespace ' ;
14+
1315 // @include
1416 private const INCLUDE_BLADE_DIRECTIVE = '/@(include|component|extends)\( \'([^ \']++) \'/ ' ;
1517
1618 // @includeIf
1719 private const CONDITIONAL_INCLUDE_BLADE_DIRECTIVE = '/@(includeIf|includeWhen)\([^,]++,\s*+ \'([^ \']++) \'/ ' ;
1820
21+ /** @var list<non-empty-string> The same as for config('view.paths') */
22+ public array $ viewPaths = [
23+ 'resources/views ' ,
24+ ];
25+
26+ /** @var array<non-empty-string, non-empty-string> Example: <element key="googletagmanager" value="resources/views/vendor/googletagmanager"/> */
27+ public array $ viewNamespaces = [];
28+
29+ /** @var non-empty-string|null Custom base directory, defaults to auto-detection */
30+ public ?string $ baseDir = null ;
31+
1932 /** @var array<string, bool> */
2033 private array $ checkedFiles = [];
2134
22- /** @var array<string, non-empty-string> */
23- public array $ templatePaths = [];
24-
2535 /** @inheritDoc */
2636 public function register (): array
2737 {
28- return [\T_OPEN_TAG , \T_INLINE_HTML ];
38+ return [\T_OPEN_TAG , \T_INLINE_HTML , \ T_STRING ];
2939 }
3040
3141 /** @inheritDoc */
@@ -43,16 +53,23 @@ public function process(File $phpcsFile, $stackPtr): int // phpcs:ignore Generic
4353 $ this ->checkedFiles [$ hash ] = true ;
4454
4555 foreach ($ tokens as $ position => $ token ) {
46- if ($ this ->isBladeIncludeDirective ($ token ['content ' ])) {
47- $ this ->validateTemplateName ($ this ->getBladeTemplateName ($ token ['content ' ]), $ phpcsFile , $ position );
48- } elseif ($ this ->isConditionalBladeIncludeDirective ($ token ['content ' ])) {
49- $ this ->validateTemplateName ($ this ->getConditionalBladeTemplateName ($ token ['content ' ]), $ phpcsFile , $ position );
50- } elseif ($ this ->isViewFacade ($ tokens , $ position )) {
51- $ this ->validateTemplateName ($ this ->getViewFacadeTemplateName ($ tokens , $ position ), $ phpcsFile , $ position );
52- } elseif ($ this ->isViewFunctionFactory ($ tokens , $ position )) {
53- $ this ->validateTemplateName ($ this ->getViewFunctionFactoryTemplateName ($ tokens , $ position ), $ phpcsFile , $ position );
54- } elseif ($ this ->isViewFunction ($ tokens , $ position )) {
55- $ this ->validateTemplateName ($ this ->getViewFunctionTemplateName ($ tokens , $ position ), $ phpcsFile , $ position );
56+ $ tokenContent = $ token ['content ' ];
57+
58+ // Handle Blade directives (found in T_INLINE_HTML tokens)
59+ if ($ this ->isBladeIncludeDirective ($ tokenContent )) {
60+ $ this ->validateTemplateName ($ this ->getBladeTemplateName ($ tokenContent ), $ phpcsFile , $ position );
61+ } elseif ($ this ->isConditionalBladeIncludeDirective ($ tokenContent )) {
62+ $ this ->validateTemplateName ($ this ->getConditionalBladeTemplateName ($ tokenContent ), $ phpcsFile , $ position );
63+ }
64+ // Handle PHP code (found in T_STRING tokens)
65+ elseif ($ token ['type ' ] === 'T_STRING ' ) {
66+ if ($ this ->isViewFacade ($ tokens , $ position )) {
67+ $ this ->validateTemplateName ($ this ->getViewFacadeTemplateName ($ tokens , $ position ), $ phpcsFile , $ position );
68+ } elseif ($ this ->isViewFunctionFactory ($ tokens , $ position )) {
69+ $ this ->validateTemplateName ($ this ->getViewFunctionFactoryTemplateName ($ tokens , $ position ), $ phpcsFile , $ position );
70+ } elseif ($ this ->isViewFunction ($ tokens , $ position )) {
71+ $ this ->validateTemplateName ($ this ->getViewFunctionTemplateName ($ tokens , $ position ), $ phpcsFile , $ position );
72+ }
5673 }
5774 }
5875
@@ -72,14 +89,14 @@ private function isConditionalBladeIncludeDirective(string $tokenContent): bool
7289 private function getBladeTemplateName (string $ tokenContent ): string
7390 {
7491 if (!$ this ->isBladeIncludeDirective ($ tokenContent )) {
75- throw new BadMethodCallException (self ::INVALID_METHOD_CALL );
92+ throw new \ BadMethodCallException (self ::INVALID_METHOD_CALL );
7693 }
7794
7895 $ matches = [];
7996 preg_match (self ::INCLUDE_BLADE_DIRECTIVE , $ tokenContent , $ matches );
8097
8198 if (!isset ($ matches [2 ])) {
82- throw new BadMethodCallException (self ::INVALID_METHOD_CALL );
99+ throw new \ BadMethodCallException (self ::INVALID_METHOD_CALL );
83100 }
84101
85102 return (string ) $ matches [2 ];
@@ -88,76 +105,142 @@ private function getBladeTemplateName(string $tokenContent): string
88105 private function getConditionalBladeTemplateName (string $ tokenContent ): string
89106 {
90107 if (!$ this ->isConditionalBladeIncludeDirective ($ tokenContent )) {
91- throw new BadMethodCallException (self ::INVALID_METHOD_CALL );
108+ throw new \ BadMethodCallException (self ::INVALID_METHOD_CALL );
92109 }
93110
94111 $ matches = [];
95112 preg_match (self ::CONDITIONAL_INCLUDE_BLADE_DIRECTIVE , $ tokenContent , $ matches );
96113
97114 if (!isset ($ matches [2 ])) {
98- throw new BadMethodCallException (self ::INVALID_METHOD_CALL );
115+ throw new \ BadMethodCallException (self ::INVALID_METHOD_CALL );
99116 }
100117
101118 return (string ) $ matches [2 ];
102119 }
103120
121+ /**
122+ * @param string $templateName In dot notation
123+ * @throws \OutOfBoundsException
124+ */
104125 private function templateIsMissing (string $ templateName ): bool
105126 {
106- return ! file_exists ($ this ->getTemplatePath ($ templateName ));
127+ foreach ($ this ->getTemplatePathCandidates ($ templateName ) as $ candidateFilePath ) {
128+ if (\file_exists ($ candidateFilePath )) {
129+ return false ;
130+ }
131+ }
132+
133+ return true ;
134+ }
135+
136+ private function resolveLaravelBaseDir (): string
137+ {
138+ return $ this ->baseDir ?? dirname (__DIR__ , 6 ); // assume this file in the classic vendor dir
107139 }
108140
109- private function getTemplatePath (string $ name ): string
141+ /**
142+ * @param string $templatePath In dot notation
143+ * @return list<non-empty-string>
144+ * @throws \OutOfBoundsException
145+ */
146+ private function getTemplatePathCandidates (string $ templatePath ): array
110147 {
111- $ namespace = preg_match ('/^(\w++)::/ ' , $ name , $ matches )
112- ? $ matches [1 ]
113- : '' ;
148+ /**
149+ * Here, we have 2 cases:
150+ * 1. No custom namespace, e.g. 'dashboard.index' -> resources/views/dashboard/index.blade.php
151+ * 2. Custom namespace, e.g. 'googletagmanager::head' -> resources/views/vendor/googletagmanager ({@see \Illuminate\Support\ServiceProvider::loadViewsFrom})
152+ */
153+
154+ $ hasCustomNamespace = preg_match ('/^(\w++)::\w+/ ' , $ templatePath , $ matches );
155+
156+ // 2. Custom namespace
157+ if ($ hasCustomNamespace ) {
158+ $ namespace = (string ) $ matches [1 ];
159+
160+ if (!isset ($ this ->viewNamespaces [$ namespace ])) {
161+ throw new \OutOfBoundsException ("Unable to find view namespace “ {$ namespace }” in viewNamespaces property. " );
162+ }
163+
164+ $ namespacePath = $ this ->viewNamespaces [$ namespace ];
114165
115- if (! isset ($ this ->templatePaths [$ namespace ])) {
116- throw new \InvalidArgumentException ("Unable to find namespace {$ namespace } in configurations! " );
166+ $ templateNameWithoutNamespace = str_replace ("{$ namespace }:: " , '' , $ templatePath );
167+
168+ return [
169+ sprintf (
170+ '%s/%s/%s.blade.php ' ,
171+ $ this ->resolveLaravelBaseDir (),
172+ $ namespacePath ,
173+ str_replace ('. ' , '/ ' , $ templateNameWithoutNamespace )
174+ ),
175+ ];
117176 }
118177
119- $ withoutNamespace = ! empty ($ namespace )
120- ? str_replace ("$ namespace:: " , '' , $ name )
121- : $ name ;
178+ // 1. No custom namespace
179+ $ candidates = [];
180+ foreach ($ this ->viewPaths as $ viewPath ) {
181+ $ candidates [] = sprintf (
182+ '%s/%s/%s.blade.php ' ,
183+ $ this ->resolveLaravelBaseDir (),
184+ $ viewPath ,
185+ str_replace ('. ' , '/ ' , $ templatePath )
186+ );
187+ }
122188
123- return sprintf (
124- '%s/%s/%s.blade.php ' ,
125- dirname (__DIR__ , 6 ),
126- $ this ->templatePaths [$ namespace ],
127- str_replace ('. ' , '/ ' , $ withoutNamespace )
189+ return $ candidates ;
190+ }
191+
192+ private function reportTemplateNotFound (File $ phpcsFile , int $ stackPtr , string $ templateName ): void
193+ {
194+ $ phpcsFile ->addError (
195+ 'Blade template "%s" not found. Expected location(s): "%s" ' ,
196+ $ stackPtr ,
197+ self ::CODE_TEMPLATE_NOT_FOUND ,
198+ [
199+ $ templateName ,
200+ implode (', ' , $ this ->getTemplatePathCandidates ($ templateName )),
201+ ]
128202 );
129203 }
130204
131- private function reportMissingTemplate (File $ phpcsFile , int $ stackPtr , string $ templateName ): void
205+ private function reportUnknownViewNamespace (File $ phpcsFile , int $ stackPtr , string $ templateName ): void
132206 {
133207 $ phpcsFile ->addWarning (
134- 'Template "%s" (%s) does not exist in "%s" ' ,
208+ 'Blade template namespace for the "%s" is not registered via "viewNamespaces" property. ' ,
135209 $ stackPtr ,
136- 'TemplateNotFound ' ,
137- [$ templateName , $ this ->getTemplatePath ($ templateName ), $ phpcsFile ->getFilename ()]
210+ self ::CODE_UNKNOWN_VIEW_NAMESPACE ,
211+ [
212+ $ templateName ,
213+ ]
138214 );
139215 }
140216
141217 private function validateTemplateName (string $ templateName , File $ phpcsFile , int $ stackPtr ): void
142218 {
143- if (rtrim ($ templateName , '-_. ' ) !== $ templateName ) {
219+ if (mb_rtrim ($ templateName , '-_. ' ) !== $ templateName ) {
144220 return ;
145221 }
146222
147223 if (str_contains ($ templateName , '$ ' )) {
148224 return ;
149225 }
150226
151- if ($ this ->templateIsMissing ($ templateName )) {
152- $ this ->reportMissingTemplate ($ phpcsFile , $ stackPtr , $ templateName );
227+ try {
228+ if ($ this ->templateIsMissing ($ templateName )) {
229+ $ this ->reportTemplateNotFound ($ phpcsFile , $ stackPtr , $ templateName );
230+ }
231+ } catch (\OutOfBoundsException ) {
232+ $ this ->reportUnknownViewNamespace ($ phpcsFile , $ stackPtr , $ templateName );
153233 }
154234 }
155235
156236 /** @param array<array<string>> $tokens */
157237 private function isViewFacade (array $ tokens , int $ position ): bool
158238 {
159- return isset ($ tokens [$ position + 2 ]) &&
160- ($ tokens [$ position ]['content ' ] === 'View ' || $ tokens [$ position ]['content ' ] === 'ViewFacade ' ) &&
239+ if (!isset ($ tokens [$ position + 3 ])) {
240+ return false ;
241+ }
242+
243+ return ($ tokens [$ position ]['content ' ] === 'View ' || $ tokens [$ position ]['content ' ] === 'ViewFacade ' ) &&
161244 $ tokens [$ position + 1 ]['type ' ] === 'T_DOUBLE_COLON ' &&
162245 $ tokens [$ position + 2 ]['content ' ] === 'make ' &&
163246 $ tokens [$ position + 3 ]['type ' ] === 'T_CONSTANT_ENCAPSED_STRING ' ;
@@ -166,27 +249,28 @@ private function isViewFacade(array $tokens, int $position): bool
166249 /** @param array<array<string>> $tokens */
167250 private function getViewFacadeTemplateName (array $ tokens , int $ position ): string // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
168251 {
169- if (! $ this ->isViewFacade ($ tokens , $ position )) {
170- throw new BadMethodCallException (self ::INVALID_METHOD_CALL );
252+ if (!$ this ->isViewFacade ($ tokens , $ position )) {
253+ throw new \ BadMethodCallException (self ::INVALID_METHOD_CALL );
171254 }
172255
173- $ lookupPosition = $ position + 4 ;
174- do {
256+ $ maxLookupPosition = $ position + 14 ;
257+ for ( $ lookupPosition = $ position + 4 ; $ lookupPosition < $ maxLookupPosition && isset ( $ tokens [ $ lookupPosition ]); $ lookupPosition ++) {
175258 if ($ tokens [$ lookupPosition ]['type ' ] !== 'T_WHITESPACE ' ) {
176- return trim ($ tokens [$ lookupPosition ]['content ' ], '\'' );
259+ return mb_trim ($ tokens [$ lookupPosition ]['content ' ], '\'" ' );
177260 }
261+ }
178262
179- $ lookupPosition ++;
180- } while (isset ($ tokens [$ lookupPosition ]) && $ lookupPosition < $ position + 14 );
181-
182- throw new BadMethodCallException ('Unable to find the template name ' );
263+ throw new \BadMethodCallException ('Unable to find the template name ' );
183264 }
184265
185266 /** @param array<array<string>> $tokens */
186267 private function isViewFunctionFactory (array $ tokens , int $ position ): bool
187268 {
188- return isset ($ tokens [$ position + 4 ]) &&
189- $ tokens [$ position ]['content ' ] === 'view ' &&
269+ if (!isset ($ tokens [$ position + 6 ])) {
270+ return false ;
271+ }
272+
273+ return $ tokens [$ position ]['content ' ] === 'view ' &&
190274 $ tokens [$ position + 1 ]['content ' ] === '( ' &&
191275 $ tokens [$ position + 2 ]['content ' ] === ') ' &&
192276 $ tokens [$ position + 3 ]['content ' ] === '-> ' &&
@@ -197,27 +281,28 @@ private function isViewFunctionFactory(array $tokens, int $position): bool
197281 /** @param array<array<string>> $tokens */
198282 private function getViewFunctionFactoryTemplateName (array $ tokens , int $ position ): string // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
199283 {
200- if (! $ this ->isViewFunctionFactory ($ tokens , $ position )) {
201- throw new BadMethodCallException (self ::INVALID_METHOD_CALL );
284+ if (!$ this ->isViewFunctionFactory ($ tokens , $ position )) {
285+ throw new \ BadMethodCallException (self ::INVALID_METHOD_CALL );
202286 }
203287
204- $ lookupPosition = $ position + 6 ;
205- do {
288+ $ maxLookupPosition = $ position + 16 ;
289+ for ( $ lookupPosition = $ position + 6 ; $ lookupPosition < $ maxLookupPosition && isset ( $ tokens [ $ lookupPosition ]); $ lookupPosition ++) {
206290 if ($ tokens [$ lookupPosition ]['type ' ] !== 'T_WHITESPACE ' ) {
207- return trim ($ tokens [$ lookupPosition ]['content ' ], '\'' );
291+ return mb_trim ($ tokens [$ lookupPosition ]['content ' ], '\'" ' );
208292 }
293+ }
209294
210- $ lookupPosition ++;
211- } while (isset ($ tokens [$ lookupPosition ]) && $ lookupPosition < $ position + 16 );
212-
213- throw new BadMethodCallException ('Unable to find the template name ' );
295+ throw new \BadMethodCallException ('Unable to find the template name ' );
214296 }
215297
216298 /** @param array<array<string>> $tokens */
217299 private function isViewFunction (array $ tokens , int $ position ): bool
218300 {
219- return isset ($ tokens [$ position - 1 ], $ tokens [$ position + 4 ]) &&
220- $ tokens [$ position - 1 ]['type ' ] === 'T_WHITESPACE ' &&
301+ if (!isset ($ tokens [$ position - 1 ], $ tokens [$ position + 2 ])) {
302+ return false ;
303+ }
304+
305+ return $ tokens [$ position - 1 ]['type ' ] === 'T_WHITESPACE ' &&
221306 $ tokens [$ position ]['content ' ] === 'view ' &&
222307 $ tokens [$ position + 1 ]['content ' ] === '( ' &&
223308 $ tokens [$ position + 2 ]['type ' ] === 'T_CONSTANT_ENCAPSED_STRING ' ;
@@ -226,10 +311,10 @@ private function isViewFunction(array $tokens, int $position): bool
226311 /** @param array<array<string>> $tokens */
227312 private function getViewFunctionTemplateName (array $ tokens , int $ position ): string
228313 {
229- if (! $ this ->isViewFunction ($ tokens , $ position )) {
230- throw new BadMethodCallException (self ::INVALID_METHOD_CALL );
314+ if (!$ this ->isViewFunction ($ tokens , $ position )) {
315+ throw new \ BadMethodCallException (self ::INVALID_METHOD_CALL );
231316 }
232317
233- return trim ($ tokens [$ position + 2 ]['content ' ], '\'' );
318+ return mb_trim ($ tokens [$ position + 2 ]['content ' ], '\'" ' );
234319 }
235320}
0 commit comments