@@ -55,6 +55,7 @@ class TestCommand extends BakeCommand
5555 'Command ' => 'Command ' ,
5656 'CommandHelper ' => 'Command\Helper ' ,
5757 'Middleware ' => 'Middleware ' ,
58+ 'Class ' => '' ,
5859 ];
5960
6061 /**
@@ -75,6 +76,7 @@ class TestCommand extends BakeCommand
7576 'Command ' => 'Command ' ,
7677 'CommandHelper ' => 'Helper ' ,
7778 'Middleware ' => 'Middleware ' ,
79+ 'Class ' => '' ,
7880 ];
7981
8082 /**
@@ -123,7 +125,11 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
123125 $ name = $ args ->getArgument ('name ' );
124126 $ name = $ this ->_getName ($ name );
125127
126- if ($ this ->bake ($ type , $ name , $ args , $ io )) {
128+ $ result = $ this ->bake ($ type , $ name , $ args , $ io );
129+ if ($ result === static ::CODE_ERROR ) {
130+ return static ::CODE_ERROR ;
131+ }
132+ if ($ result ) {
127133 $ io ->success ('Done ' );
128134 }
129135
@@ -212,10 +218,29 @@ protected function _getClassOptions(string $namespace): array
212218 }
213219
214220 $ path = $ base . str_replace ('\\' , DS , $ namespace );
215- $ files = (new Filesystem ())->find ($ path );
216- foreach ($ files as $ fileObj ) {
217- if ($ fileObj ->isFile ()) {
218- $ classes [] = substr ($ fileObj ->getFileName (), 0 , -4 ) ?: '' ;
221+
222+ // For generic Class type (empty namespace), search recursively
223+ if ($ namespace === '' ) {
224+ $ files = (new Filesystem ())->findRecursive ($ path , '/\.php$/ ' );
225+ foreach ($ files as $ fileObj ) {
226+ if ($ fileObj ->isFile () && $ fileObj ->getFileName () !== 'Application.php ' ) {
227+ // Build the namespace path relative to App directory
228+ $ relativePath = str_replace ($ base , '' , $ fileObj ->getPath ());
229+ $ relativePath = trim (str_replace (DS , '\\' , $ relativePath ), '\\' );
230+ $ className = substr ($ fileObj ->getFileName (), 0 , -4 ) ?: '' ;
231+ if ($ relativePath ) {
232+ $ classes [] = $ relativePath . '\\' . $ className ;
233+ } else {
234+ $ classes [] = $ className ;
235+ }
236+ }
237+ }
238+ } else {
239+ $ files = (new Filesystem ())->find ($ path );
240+ foreach ($ files as $ fileObj ) {
241+ if ($ fileObj ->isFile ()) {
242+ $ classes [] = substr ($ fileObj ->getFileName (), 0 , -4 ) ?: '' ;
243+ }
219244 }
220245 }
221246 sort ($ classes );
@@ -230,18 +255,43 @@ protected function _getClassOptions(string $namespace): array
230255 * @param string $className the 'cake name' for the class ie. Posts for the PostsController
231256 * @param \Cake\Console\Arguments $args Arguments
232257 * @param \Cake\Console\ConsoleIo $io ConsoleIo instance
233- * @return string|bool
258+ * @return string|bool|int Returns the generated code as string on success, false on failure, or CODE_ERROR for validation errors
234259 */
235- public function bake (string $ type , string $ className , Arguments $ args , ConsoleIo $ io ): string |bool
260+ public function bake (string $ type , string $ className , Arguments $ args , ConsoleIo $ io ): string |bool | int
236261 {
237262 $ type = $ this ->normalize ($ type );
238263 if (!isset ($ this ->classSuffixes [$ type ]) || !isset ($ this ->classTypes [$ type ])) {
239264 return false ;
240265 }
241266
267+ // For Class type, validate that backslashes are properly escaped
268+ if ($ type === 'Class ' && !str_contains ($ className , '\\' )) {
269+ $ io ->error ('Class name appears to have no namespace separators. ' );
270+ $ io ->out ('' );
271+ $ io ->out ('If you meant to specify a namespaced class, please use quotes: ' );
272+ $ io ->out (" <info>bin/cake bake test class ' {$ className }'</info> " );
273+ $ io ->out ('' );
274+ $ io ->out ('Or specify without the base namespace: ' );
275+ $ io ->out (' <info>bin/cake bake test class YourNamespace \\ClassName</info> ' );
276+
277+ return static ::CODE_ERROR ;
278+ }
279+
242280 $ prefix = $ this ->getPrefix ($ args );
243281 $ fullClassName = $ this ->getRealClassName ($ type , $ className , $ prefix );
244282
283+ // For Class type, validate that the class exists
284+ if ($ type === 'Class ' && !class_exists ($ fullClassName )) {
285+ $ io ->error ("Class ' {$ fullClassName }' does not exist or cannot be loaded. " );
286+ $ io ->out ('' );
287+ $ io ->out ('Please check: ' );
288+ $ io ->out (' - The class file exists in the correct location ' );
289+ $ io ->out (' - The class is properly autoloaded ' );
290+ $ io ->out (' - The namespace and class name are correct ' );
291+
292+ return static ::CODE_ERROR ;
293+ }
294+
245295 // Check if fixture factories plugin is available
246296 $ hasFixtureFactories = $ this ->hasFixtureFactories ();
247297
@@ -266,8 +316,14 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo
266316 [$ preConstruct , $ construction , $ postConstruct ] = $ this ->generateConstructor ($ type , $ fullClassName );
267317 $ uses = $ this ->generateUses ($ type , $ fullClassName );
268318
269- $ subject = $ className ;
270- [$ namespace , $ className ] = namespaceSplit ($ fullClassName );
319+ // For generic Class type, extract just the class name for the subject
320+ if ($ type === 'Class ' ) {
321+ [$ namespace , $ className ] = namespaceSplit ($ fullClassName );
322+ $ subject = $ className ;
323+ } else {
324+ $ subject = $ className ;
325+ [$ namespace , $ className ] = namespaceSplit ($ fullClassName );
326+ }
271327
272328 $ baseNamespace = Configure::read ('App.namespace ' );
273329 if ($ this ->plugin ) {
@@ -381,6 +437,17 @@ public function getRealClassName(string $type, string $class, ?string $prefix =
381437 if ($ this ->plugin ) {
382438 $ namespace = str_replace ('/ ' , '\\' , $ this ->plugin );
383439 }
440+
441+ // For generic Class type, the class name contains the full subnamespace path
442+ if ($ type === 'Class ' ) {
443+ // Strip base namespace if user included it
444+ if (str_starts_with ($ class , $ namespace . '\\' )) {
445+ $ class = substr ($ class , strlen ($ namespace ) + 1 );
446+ }
447+
448+ return $ namespace . '\\' . $ class ;
449+ }
450+
384451 $ suffix = $ this ->classSuffixes [$ type ];
385452 $ subSpace = $ this ->mapType ($ type );
386453 if ($ suffix && strpos ($ class , $ suffix ) === false ) {
@@ -415,7 +482,7 @@ public function getSubspacePath(string $type): string
415482 */
416483 public function mapType (string $ type ): string
417484 {
418- if (empty ($ this ->classTypes [$ type ])) {
485+ if (! isset ($ this ->classTypes [$ type ])) {
419486 throw new CakeException ('Invalid object type: ' . $ type );
420487 }
421488
@@ -585,6 +652,18 @@ public function generateConstructor(string $type, string $fullClassName): array
585652 $ pre .= ' $this->io = new ConsoleIo($this->stub); ' ;
586653 $ construct = "new {$ className }( \$this->io); " ;
587654 }
655+ if ($ type === 'Class ' ) {
656+ // Check if class has required constructor parameters
657+ if (class_exists ($ fullClassName )) {
658+ $ reflection = new ReflectionClass ($ fullClassName );
659+ $ constructor = $ reflection ->getConstructor ();
660+ if (!$ constructor || $ constructor ->getNumberOfRequiredParameters () === 0 ) {
661+ $ construct = "new {$ className }(); " ;
662+ }
663+ } else {
664+ $ construct = "new {$ className }(); " ;
665+ }
666+ }
588667
589668 return [$ pre , $ construct , $ post ];
590669 }
@@ -635,7 +714,17 @@ public function generateProperties(string $type, string $subject, string $fullCl
635714 break ;
636715 }
637716
638- if (!in_array ($ type , ['Controller ' , 'Command ' ])) {
717+ // Skip test subject property for Controller, Command, and Class types with required constructor params
718+ $ skipProperty = in_array ($ type , ['Controller ' , 'Command ' ], true );
719+ if ($ type === 'Class ' && class_exists ($ fullClassName )) {
720+ $ reflection = new ReflectionClass ($ fullClassName );
721+ $ constructor = $ reflection ->getConstructor ();
722+ if ($ constructor && $ constructor ->getNumberOfRequiredParameters () > 0 ) {
723+ $ skipProperty = true ;
724+ }
725+ }
726+
727+ if (!$ skipProperty ) {
639728 $ properties [] = [
640729 'description ' => 'Test subject ' ,
641730 'type ' => '\\' . $ fullClassName ,
0 commit comments