55namespace Tempest \Core ;
66
77use Closure ;
8- use Nette \InvalidStateException ;
98use Tempest \Console \HasConsole ;
9+ use Tempest \Container \Inject ;
1010use Tempest \Generation \ClassManipulator ;
11- use function Tempest \src_namespace ;
12- use function Tempest \src_path ;
11+ use Tempest \Generation \DataObjects \StubFile ;
12+ use Tempest \Generation \Enums \StubFileType ;
13+ use Tempest \Generation \Exceptions \FileGenerationAbortedException ;
14+ use Tempest \Generation \Exceptions \FileGenerationFailedException ;
15+ use Tempest \Generation \StubFileGenerator ;
16+ use Tempest \Support \PathHelper ;
1317use function Tempest \Support \str ;
18+ use Tempest \Validation \Rules \EndsWith ;
19+ use Tempest \Validation \Rules \NotEmpty ;
20+ use Throwable ;
1421
22+ /**
23+ * Provides a bunch of methods to publish and generate files and work with common user input.
24+ */
1525trait PublishesFiles
1626{
1727 use HasConsole;
1828
29+ #[Inject]
30+ private readonly Composer $ composer ;
31+
32+ #[Inject]
33+ private readonly StubFileGenerator $ stubFileGenerator ;
34+
1935 private array $ publishedFiles = [];
2036
2137 private array $ publishedClasses = [];
2238
2339 /**
24- * @param Closure(string $source, string $destination): void|null $callback
40+ * Publishes a file from a source to a destination.
41+ * @param string $source The path to the source file.
42+ * @param string $destination The path to the destination file.
43+ * @param Closure(string $source, string $destination): void|null $callback A callback to run after the file is published.
2544 */
2645 public function publish (
2746 string $ source ,
2847 string $ destination ,
2948 ?Closure $ callback = null ,
3049 ): void {
31- if (file_exists ($ destination )) {
32- if (! $ this ->confirm (
33- question: "{$ destination } already exists Do you want to overwrite it? " ,
34- )) {
35- return ;
36- }
37- } else {
38- if (! $ this ->confirm (
39- question: "Do you want to create {$ destination }? " ,
50+ try {
51+ if (! $ this ->console ->confirm (
52+ question: sprintf ('Do you want to create "%s" ' , $ destination ),
4053 default: true ,
4154 )) {
42- $ this ->writeln ('Skipped ' );
55+ throw new FileGenerationAbortedException ('Skipped. ' );
56+ }
4357
44- return ;
58+ if (! $ this ->askForOverride ($ destination )) {
59+ throw new FileGenerationAbortedException ('Skipped. ' );
4560 }
46- }
4761
48- $ dir = pathinfo ( $ destination , PATHINFO_DIRNAME );
62+ $ stubFile = StubFile:: from ( $ source );
4963
50- if (! is_dir ( $ dir )) {
51- mkdir ( $ dir , recursive: true );
52- }
64+ // Handle class files
65+ if ( $ stubFile -> type === StubFileType:: CLASS_FILE ) {
66+ $ oldClass = new ClassManipulator ( $ source );
5367
54- copy ($ source , $ destination );
68+ $ this ->stubFileGenerator ->generateClassFile (
69+ stubFile: $ stubFile ,
70+ targetPath: $ destination ,
71+ shouldOverride: true ,
72+ manipulations: [
73+ fn (ClassManipulator $ class ) => $ class ->removeClassAttribute (DoNotDiscover::class),
74+ ],
75+ );
5576
56- $ this -> updateClass ($ destination );
77+ $ newClass = new ClassManipulator ($ destination );
5778
58- $ this ->publishedFiles [] = $ destination ;
79+ $ this ->publishedClasses [$ oldClass ->getClassName ()] = $ newClass ->getClassName ();
80+ }
5981
60- if ($ callback !== null ) {
61- $ callback ($ source , $ destination );
62- }
82+ // Handle raw files
83+ if ($ stubFile ->type === StubFileType::RAW_FILE ) {
84+ $ this ->stubFileGenerator ->generateRawFile (
85+ stubFile: $ stubFile ,
86+ targetPath: $ destination ,
87+ shouldOverride: true ,
88+ );
89+ }
90+
91+ $ this ->publishedFiles [] = $ destination ;
6392
64- $ this ->success ("{$ destination } created " );
93+ if ($ callback !== null ) {
94+ $ callback ($ source , $ destination );
95+ }
96+
97+ $ this ->console ->success (sprintf ('File successfully created at "%s". ' , $ destination ));
98+ } catch (FileGenerationAbortedException $ exception ) {
99+ $ this ->console ->info ($ exception ->getMessage ());
100+ } catch (Throwable $ throwable ) {
101+ throw new FileGenerationFailedException (
102+ message: 'The file could not be published. ' ,
103+ previous: $ throwable ,
104+ );
105+ }
65106 }
66107
108+ /**
109+ * Publishes the imports of the published classes.
110+ * Any published class that is imported in another published class will have its import updated.
111+ */
67112 public function publishImports (): void
68113 {
69114 foreach ($ this ->publishedFiles as $ file ) {
@@ -77,30 +122,64 @@ public function publishImports(): void
77122 }
78123 }
79124
80- private function updateClass (string $ destination ): void
125+ /**
126+ * Gets a suggested path for the given class name.
127+ * This will use the user's main namespace as the base path.
128+ * @param string $className The class name to generate the path for, can include path parts (e.g. 'Models/User').
129+ * @param string|null $pathPrefix The prefix to add to the path (e.g. 'Models').
130+ * @param string|null $classSuffix The suffix to add to the class name (e.g. 'Model').
131+ * @return string The fully suggested path including the filename and extension.
132+ */
133+ public function getSuggestedPath (string $ className , ?string $ pathPrefix = null , ?string $ classSuffix = null ): string
81134 {
82- try {
83- $ class = new ClassManipulator ($ destination );
84- } catch (InvalidStateException ) {
85- return ;
86- }
135+ // Separate input path and classname
136+ $ inputClassName = PathHelper::toClassName ($ className );
137+ $ inputPath = str (PathHelper::make ($ className ))->replaceLast ($ inputClassName , '' )->toString ();
138+ $ className = str ($ inputClassName )
139+ ->pascal ()
140+ ->finish ($ classSuffix ?? '' )
141+ ->toString ();
87142
88- $ namespace = str ($ destination )
89- ->replaceStart (rtrim (src_path (), '/ ' ), src_namespace ())
90- ->replaceEnd ('.php ' , '' )
91- ->replace ('/ ' , '\\' )
92- ->explode ('\\' )
93- ->pop ($ value )
94- ->implode ('\\' )
143+ // Prepare the suggested path from the project namespace
144+ return str (PathHelper::make (
145+ $ this ->composer ->mainNamespace ->path ,
146+ $ pathPrefix ?? '' ,
147+ $ inputPath ,
148+ ))
149+ ->finish ('/ ' )
150+ ->append ($ className . '.php ' )
95151 ->toString ();
152+ }
153+
154+ /**
155+ * Prompt the user for the target path to save the generated file.
156+ * @param string $suggestedPath The suggested path to show to the user.
157+ * @return string The target path that the user has chosen.
158+ */
159+ public function promptTargetPath (string $ suggestedPath ): string
160+ {
161+ $ className = PathHelper::toClassName ($ suggestedPath );
96162
97- $ oldClassName = $ class ->getClassName ();
163+ return $ this ->console ->ask (
164+ question: sprintf ('Where do you want to save the file "%s"? ' , $ className ),
165+ default: $ suggestedPath ,
166+ validation: [new NotEmpty (), new EndsWith ('.php ' )],
167+ );
168+ }
98169
99- $ class
100- ->setNamespace ($ namespace )
101- ->removeClassAttribute (DoNotDiscover::class)
102- ->save ($ destination );
170+ /**
171+ * Ask the user if they want to override the file if it already exists.
172+ * @param string $targetPath The target path to check for existence.
173+ * @return bool Whether the user wants to override the file.
174+ */
175+ public function askForOverride (string $ targetPath ): bool
176+ {
177+ if (! file_exists ($ targetPath )) {
178+ return true ;
179+ }
103180
104- $ this ->publishedClasses [$ oldClassName ] = $ class ->getClassName ();
181+ return $ this ->console ->confirm (
182+ question: sprintf ('The file "%s" already exists. Do you want to override it? ' , $ targetPath ),
183+ );
105184 }
106185}
0 commit comments