1212use Vildanbina \ComposerUpgrader \Service \ComposerFileService ;
1313use Vildanbina \ComposerUpgrader \Service \Config ;
1414use Vildanbina \ComposerUpgrader \Service \VersionService ;
15+ use Composer \Package \Link ;
16+ use Composer \Semver \Constraint \Constraint ;
1517
1618class UpgradeAllCommand extends BaseCommand
1719{
@@ -64,6 +66,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6466 $ this ->versionService ->setComposer ($ composer );
6567 $ this ->versionService ->setIO ($ this ->getIO ());
6668 $ hasUpdates = false ;
69+ $ proposedChanges = [];
6770
6871 foreach ($ dependencies as $ package => $ constraint ) {
6972 if ($ config ->only && ! in_array ($ package , $ config ->only )) {
@@ -109,6 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
109112
110113 if (! $ config ->dryRun && $ shouldUpdate && $ versionToUse ) {
111114 $ cleanVersion = preg_replace ('/^v/ ' , '' , $ versionToUse );
115+ $ proposedChanges [$ package ] = '^ ' .$ cleanVersion ;
112116 $ this ->composerFileService ->updateDependency ($ composerJson , $ package , '^ ' .$ cleanVersion );
113117 }
114118 } catch (UnexpectedValueException $ e ) {
@@ -118,21 +122,110 @@ protected function execute(InputInterface $input, OutputInterface $output): int
118122 }
119123 }
120124
121- if (! $ config ->dryRun ) {
122- if ($ hasUpdates ) {
123- $ this ->composerFileService ->saveComposerJson ($ composerJson , $ composerJsonPath );
124- $ output ->writeln ('Composer.json has been updated. Please run "composer update" to apply changes. ' );
125- } else {
125+ if ($ hasUpdates && ! $ config ->dryRun ) {
126+ // Perform validation before finalizing the save
127+ if (!$ this ->validateNewConstraints ($ composer , $ proposedChanges , $ output )) {
128+ $ output ->writeln ('<error>Aborting: The proposed upgrades would cause conflicts.</error> ' );
129+ return 1 ;
130+ }
131+
132+ $ this ->composerFileService ->saveComposerJson ($ composerJson , $ composerJsonPath );
133+ $ output ->writeln ('Composer.json has been updated. Please run "composer update" to apply changes. ' );
134+ } else {
135+ if (! $ hasUpdates ) {
126136 $ message = 'No dependency updates were required. ' ;
127137 if ($ output ->isVerbose ()) {
128138 $ message .= ' All dependencies already satisfy the requested constraints. ' ;
129139 }
130140 $ output ->writeln ($ message );
131141 }
132- } else {
133- $ output ->writeln ('Dry run complete. No changes applied. ' );
142+
143+ if ($ config ->dryRun ) {
144+ $ output ->writeln ('Dry run complete. No changes applied. ' );
145+ }
134146 }
135147
136148 return 0 ;
137149 }
138- }
150+
151+ /**
152+ * Validates the proposed package constraints using the Composer Solver.
153+ *
154+ * @param \Composer\Composer $composer
155+ * @param array<string, string> $proposedChanges
156+ * @param OutputInterface $output
157+ * @return bool
158+ */
159+ private function validateNewConstraints (\Composer \Composer $ composer , array $ proposedChanges , OutputInterface $ output ): bool
160+ {
161+ if (empty ($ proposedChanges )) {
162+ return true ;
163+ }
164+
165+ $ repoManager = $ composer ->getRepositoryManager ();
166+ $ localRepo = $ repoManager ? $ repoManager ->getLocalRepository () : null ;
167+
168+ // If the local repository cannot provide a package list (common in incomplete mocks),
169+ // we bypass validation to avoid a fatal crash in the Composer internal solver.
170+ if (!$ localRepo || !is_iterable ($ localRepo ->getPackages ())) {
171+ return true ;
172+ }
173+
174+ $ rootPackage = $ composer ->getPackage ();
175+ $ originalRequires = $ rootPackage ->getRequires ();
176+
177+ try {
178+ $ output ->writeln ('<info>Validating dependency compatibility...</info> ' );
179+
180+ $ newRequires = $ originalRequires ;
181+ foreach ($ proposedChanges as $ package => $ version ) {
182+ $ newRequires [$ package ] = new Link (
183+ '__root__ ' ,
184+ $ package ,
185+ new Constraint ('>= ' , preg_replace ('/^\^/ ' , '' , $ version )),
186+ Link::TYPE_REQUIRE ,
187+ $ version
188+ );
189+ }
190+ $ rootPackage ->setRequires ($ newRequires );
191+
192+ $ installer = \Composer \Installer::create ($ this ->getIO (), $ composer );
193+ $ installer
194+ ->setDryRun (true )
195+ ->setUpdate (true )
196+ ->setInstall (false );
197+
198+ $ status = $ installer ->run ();
199+
200+ // Revert in-memory state
201+ $ rootPackage ->setRequires ($ originalRequires );
202+
203+ return $ status === 0 ;
204+
205+ } catch (\Exception $ e ) {
206+ $ rootPackage ->setRequires ($ originalRequires );
207+
208+ $ output ->writeln ("\n<error>Incompatibility detected for the following proposed changes:</error> " );
209+
210+ $ errorMessage = $ e ->getMessage ();
211+ $ foundProblematic = false ;
212+
213+ foreach (array_keys ($ proposedChanges ) as $ packageName ) {
214+ // Check if the specific package we tried to upgrade is mentioned in the error
215+ if (str_contains ($ errorMessage , $ packageName )) {
216+ $ output ->writeln (" - <options=bold> {$ packageName }</> " );
217+ $ foundProblematic = true ;
218+ }
219+ }
220+
221+ if (!$ foundProblematic ) {
222+ $ output ->writeln (" - <info>The conflict involves sub-dependencies of your packages.</info> " );
223+ }
224+
225+ $ output ->writeln ("\n<comment>Composer Reason:</comment> " );
226+ $ output ->writeln ($ errorMessage );
227+
228+ return false ;
229+ }
230+ }
231+ }
0 commit comments