aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/factory.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-28 03:31:41 +0900
committernsfisis <nsfisis@gmail.com>2026-05-28 03:32:04 +0900
commitc7f53c5d7d581ebf76803650c63ec615b1558dc8 (patch)
treec6d83819e82a83cf93ca737b661094a8ea800cec /crates/shirabe/src/factory.rs
parentcc5d73c05a0abca2eebcc8a6afa0b1543ee49850 (diff)
downloadphp-shirabe-c7f53c5d7d581ebf76803650c63ec615b1558dc8.tar.gz
php-shirabe-c7f53c5d7d581ebf76803650c63ec615b1558dc8.tar.zst
php-shirabe-c7f53c5d7d581ebf76803650c63ec615b1558dc8.zip
refactor(composer): represent composer via trait-based handlesrefactor/composer-handles
Replace the PartialComposer/Composer structs and the single Rc<RefCell<PartialOrFullComposer>> enum with PartialComposer and Composer traits (Composer: PartialComposer), InnerPartialComposer / InnerFullComposer data structs, and the handle types FullComposerHandle (impl Composer) and AnyComposerHandle (polymorphic enum, impl PartialComposer), plus their weak variants. Factory builds the full and partial graphs via separate Rc::new_cyclic branches that share a build_composer_base helper. Call sites now use trait methods that encapsulate borrowing instead of borrow_partial() / composer_full*(). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/factory.rs')
-rw-r--r--crates/shirabe/src/factory.rs434
1 files changed, 237 insertions, 197 deletions
diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs
index 80f1548..73197ce 100644
--- a/crates/shirabe/src/factory.rs
+++ b/crates/shirabe/src/factory.rs
@@ -15,8 +15,10 @@ use shirabe_php_shim::{
use crate::autoload::AutoloadGenerator;
use crate::cache::Cache;
-use crate::composer::{ComposerWeakHandle, PartialOrFullComposer};
-use crate::composer::{PartialComposerHandle, PartialComposerWeakHandle};
+use crate::composer::{
+ AnyComposerHandle, AnyComposerWeakHandle, Composer, FullComposerHandle, FullComposerWeakHandle,
+ InnerFullComposer, InnerPartialComposer, PartialComposer,
+};
use crate::config::Config;
use crate::config::JsonConfigSource;
use crate::downloader::DownloadManager;
@@ -90,6 +92,16 @@ impl DisablePlugins {
}
}
+/// Shared managers produced while building the common (partial) part of a Composer, reused by the
+/// full-load branch to wire the full-only managers (download/autoload/archive/locker).
+struct ComposerBuildArtifacts {
+ http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>,
+ process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>,
+ r#loop: std::rc::Rc<std::cell::RefCell<Loop>>,
+ dispatcher: std::rc::Rc<std::cell::RefCell<EventDispatcher>>,
+ im: std::rc::Rc<std::cell::RefCell<InstallationManager>>,
+}
+
/// Creates a configured instance of composer.
pub struct Factory;
@@ -436,7 +448,7 @@ impl Factory {
cwd: Option<&str>,
full_load: bool,
disable_scripts: bool,
- ) -> anyhow::Result<PartialComposerHandle> {
+ ) -> anyhow::Result<AnyComposerHandle> {
// if a custom composer.json path is given, we change the default cwd to be that file's directory
let mut local_config = local_config;
let mut cwd = cwd.map(|s| s.to_string());
@@ -577,161 +589,50 @@ impl Factory {
// PartialComposerWeak (Weak<RefCell<InnerComposer>>). The closure cannot return a
// Result, so construction errors are surfaced through `build_error`.
let mut build_error: Option<anyhow::Error> = None;
- let composer = std::rc::Rc::new_cyclic(
- |composer_weak: &std::rc::Weak<std::cell::RefCell<PartialOrFullComposer>>| {
- let mut build = || -> anyhow::Result<PartialOrFullComposer> {
- let mut composer: PartialOrFullComposer = if full_load {
- PartialOrFullComposer::new_full()
- } else {
- PartialOrFullComposer::new_partial()
- };
- composer.set_config(config.clone());
- if is_global {
- composer.set_global();
- }
-
- if full_load {
- // load auth configs into the IO instance
- // TODO(phase-b): load_configuration requires &mut IOInterface; create_composer takes &dyn IOInterface
- // io.load_configuration(&mut *config.borrow_mut())?;
-
- // load existing Composer\InstalledVersions instance if available and scripts/plugins are allowed, as they might need it
- // we only load if the InstalledVersions class wasn't defined yet so that this is only loaded once
- let installed_versions_path = format!(
- "{}/composer/installed.php",
- config.borrow_mut().get_str("vendor-dir")?
- );
- if !disable_plugins.is_disabled_at_all()
- && !disable_scripts
- && !class_exists("Composer\\InstalledVersions")
- && file_exists(&installed_versions_path)
- {
- // force loading the class at this point so it is loaded from the composer phar and not from the vendor dir
- // as we cannot guarantee integrity of that file
- if class_exists("Composer\\InstalledVersions") {
- FilesystemRepository::safely_load_installed_versions(
- &installed_versions_path,
- );
- }
- }
- }
-
- let http_downloader = std::rc::Rc::new(std::cell::RefCell::new(
- Self::create_http_downloader(io.clone(), &config, IndexMap::new())?,
+ let composer: AnyComposerHandle = if full_load {
+ let inner = std::rc::Rc::new_cyclic(
+ |composer_weak: &std::rc::Weak<std::cell::RefCell<InnerFullComposer>>| {
+ let weak = AnyComposerWeakHandle::Full(FullComposerWeakHandle::from_weak(
+ composer_weak.clone(),
));
- let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(
- Some(io.clone()),
- )));
- let r#loop = std::rc::Rc::new(std::cell::RefCell::new(Loop::new(
- http_downloader.clone(),
- Some(process.clone()),
- )));
- composer.set_loop(r#loop.clone());
-
- // initialize event dispatcher with the Composer back-reference
- let dispatcher = {
- let mut d = EventDispatcher::new(
- PartialComposerWeakHandle::from_weak(composer_weak.clone()),
+ let mut full = InnerFullComposer::default();
+ let mut build = || -> anyhow::Result<()> {
+ let artifacts = self.build_composer_base(
+ &mut full.partial,
+ weak,
io.clone(),
- Some(process.clone()),
- );
- d.set_run_scripts(!disable_scripts);
- std::rc::Rc::new(std::cell::RefCell::new(d))
- };
- composer.set_event_dispatcher(dispatcher.clone());
-
- // initialize repository manager
- let rm = std::rc::Rc::new(std::cell::RefCell::new(RepositoryFactory::manager(
- io.clone(),
- &config,
- Some(http_downloader.clone()),
- Some(dispatcher.clone()),
- Some(process.clone()),
- )?));
-
- // force-set the version of the global package if not defined as
- // guessing it adds no value and only takes time
- if !full_load && !local_config_data.contains_key("version") {
- local_config_data
- .insert("version".to_string(), PhpMixed::String("1.0.0".to_string()));
- }
-
- // load package
- let parser = VersionParser::new();
- let guesser = VersionGuesser::new(
- config.clone(),
- process.clone(),
- parser.clone(),
- Some(io.clone()),
- );
- let mut loader = self.load_root_package(
- rm.clone(),
- config.clone(),
- parser,
- guesser,
- io.clone(),
- );
- let package = loader.load(
- local_config_data
- .iter()
- .map(|(k, v)| (k.clone(), Box::new(v.clone())))
- .collect(),
- "Composer\\Package\\RootPackage",
- Some(&cwd),
- )?;
- // TODO(phase-b): set_package expects RootPackageInterface; loader returns BasePackage
- // composer.set_package(package);
- let _ = package;
-
- // load local repository
- self.add_local_repository(
- io.clone(),
- &mut rm.borrow_mut(),
- &vendor_dir,
- composer.get_package().clone(),
- Some(&process),
- );
- composer.set_repository_manager(rm.clone());
-
- // initialize installation manager
- let im = std::rc::Rc::new(std::cell::RefCell::new(
- self.create_installation_manager(
- r#loop.clone(),
- io.clone(),
- Some(dispatcher.clone()),
- ),
- ));
- composer.set_installation_manager(im.clone());
+ &config,
+ full_load,
+ is_global,
+ disable_plugins,
+ disable_scripts,
+ &mut local_config_data,
+ &cwd,
+ &vendor_dir,
+ )?;
- if let PartialOrFullComposer::Full(ref mut composer_full) = composer {
// initialize download manager
let dm = self.create_download_manager(
io.clone(),
&config,
- &http_downloader,
- &process,
- Some(&dispatcher),
+ &artifacts.http_downloader,
+ &artifacts.process,
+ Some(&artifacts.dispatcher),
)?;
- composer_full.set_download_manager(dm.clone());
+ full.download_manager = Some(dm.clone());
// initialize autoload generator
let generator =
- AutoloadGenerator::new(dispatcher.clone(), Some(io.clone()));
- composer_full.set_autoload_generator(std::rc::Rc::new(
- std::cell::RefCell::new(generator),
- ));
+ AutoloadGenerator::new(artifacts.dispatcher.clone(), Some(io.clone()));
+ full.autoload_generator =
+ Some(std::rc::Rc::new(std::cell::RefCell::new(generator)));
// initialize archive manager
- let am = self.create_archive_manager(&*config.borrow(), &dm, &r#loop)?;
- composer_full
- .set_archive_manager(std::rc::Rc::new(std::cell::RefCell::new(am)));
- }
-
- // add installers to the manager (must happen after download manager is created since they read it out of $composer)
- self.create_default_installers(&im, &composer, io.clone(), Some(&process));
+ let am =
+ self.create_archive_manager(&*config.borrow(), &dm, &artifacts.r#loop)?;
+ full.archive_manager = Some(std::rc::Rc::new(std::cell::RefCell::new(am)));
- // init locker if possible
- if let PartialOrFullComposer::Full(ref mut composer_full) = composer {
+ // init locker if possible
if let Some(ref composer_file_path) = composer_file {
let lock_file = Self::get_lock_file(composer_file_path);
let lock_enabled = config
@@ -741,13 +642,13 @@ impl Factory {
.unwrap_or(true);
if !lock_enabled && file_exists(&lock_file) {
io.write_error3(
- &format!(
- "<warning>{} is present but ignored as the \"lock\" config option is disabled.</warning>",
- lock_file
- ),
- true,
- crate::io::NORMAL,
- );
+ &format!(
+ "<warning>{} is present but ignored as the \"lock\" config option is disabled.</warning>",
+ lock_file
+ ),
+ true,
+ crate::io::NORMAL,
+ );
}
let locker = Locker::new(
@@ -761,12 +662,11 @@ impl Factory {
None,
Some(io.clone()),
)?,
- im.clone(),
+ artifacts.im.clone(),
&file_get_contents(composer_file_path).unwrap_or_default(),
- process.clone(),
+ artifacts.process.clone(),
);
- composer_full
- .set_locker(std::rc::Rc::new(std::cell::RefCell::new(locker)));
+ full.locker = Some(std::rc::Rc::new(std::cell::RefCell::new(locker)));
} else {
let lock_contents = JsonFile::encode(
&PhpMixed::Array(
@@ -780,26 +680,46 @@ impl Factory {
let locker = Locker::new(
io.clone(),
JsonFile::new(Platform::get_dev_null(), None, Some(io.clone()))?,
- im.clone(),
+ artifacts.im.clone(),
&lock_contents,
- process.clone(),
+ artifacts.process.clone(),
);
- composer_full
- .set_locker(std::rc::Rc::new(std::cell::RefCell::new(locker)));
+ full.locker = Some(std::rc::Rc::new(std::cell::RefCell::new(locker)));
}
+ Ok(())
+ };
+ if let Err(e) = build() {
+ build_error = Some(e);
}
-
- Ok(composer)
- };
- match build() {
- Ok(composer) => std::cell::RefCell::new(composer),
- Err(e) => {
+ std::cell::RefCell::new(full)
+ },
+ );
+ AnyComposerHandle::Full(FullComposerHandle::from_rc(inner))
+ } else {
+ let inner = std::rc::Rc::new_cyclic(
+ |composer_weak: &std::rc::Weak<std::cell::RefCell<InnerPartialComposer>>| {
+ let weak = AnyComposerWeakHandle::Partial(composer_weak.clone());
+ let mut partial = InnerPartialComposer::default();
+ if let Err(e) = self.build_composer_base(
+ &mut partial,
+ weak,
+ io.clone(),
+ &config,
+ full_load,
+ is_global,
+ disable_plugins,
+ disable_scripts,
+ &mut local_config_data,
+ &cwd,
+ &vendor_dir,
+ ) {
build_error = Some(e);
- std::cell::RefCell::new(PartialOrFullComposer::new_partial())
}
- }
- },
- );
+ std::cell::RefCell::new(partial)
+ },
+ );
+ AnyComposerHandle::Partial(inner)
+ };
if let Some(e) = build_error {
return Err(e);
}
@@ -809,11 +729,8 @@ impl Factory {
// PluginManager::new upgrades the Composer back-reference to read its config and locker, so
// it must be built after Rc::new_cyclic returns; inside the closure the Rc is not yet
// constructed and the weak handle cannot upgrade.
- let (is_full, is_global) = {
- let c = composer.borrow();
- (c.is_full(), c.is_global())
- };
- if is_full {
+ let is_global = composer.is_global();
+ if let Some(full) = composer.as_full() {
let global_composer = if !is_global {
self.create_global_composer(
io.clone(),
@@ -828,16 +745,12 @@ impl Factory {
let pm = self.create_plugin_manager(
io.clone(),
- ComposerWeakHandle::from_weak(std::rc::Rc::downgrade(&composer)),
+ full.downgrade(),
global_composer,
disable_plugins,
);
let pm = std::rc::Rc::new(std::cell::RefCell::new(pm));
- composer
- .borrow_mut()
- .as_full_mut()
- .unwrap()
- .set_plugin_manager(pm.clone());
+ full.set_plugin_manager(pm.clone());
if is_global {
pm.borrow_mut().set_running_in_global_dir(true);
@@ -850,7 +763,7 @@ impl Factory {
// the Composer through the dispatcher) is safe only after the Rc has been constructed.
let init_event = Event::from_name(PluginEvents::INIT.to_string());
let init_event_name = init_event.get_name().to_string();
- let dispatcher = composer.borrow().get_event_dispatcher();
+ let dispatcher = composer.get_event_dispatcher();
dispatcher
.borrow_mut()
.dispatch(Some(&init_event_name), Some(init_event))?;
@@ -861,14 +774,14 @@ impl Factory {
// self.purge_packages(rm.get_local_repository(), &mut im)?;
}
- Ok(PartialComposerHandle::from_rc(composer))
+ Ok(composer)
}
pub fn create_global(
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
disable_plugins: DisablePlugins,
disable_scripts: bool,
- ) -> Option<PartialComposerHandle> {
+ ) -> Option<AnyComposerHandle> {
let factory = Self;
let config = Self::create_config(Some(io.clone()), None).ok()?;
@@ -909,7 +822,7 @@ impl Factory {
disable_plugins: DisablePlugins,
disable_scripts: bool,
full_load: bool,
- ) -> Option<PartialComposerHandle> {
+ ) -> Option<AnyComposerHandle> {
// make sure if disable plugins was 'local' it is now turned off
let disable_plugins = if matches!(
disable_plugins,
@@ -1176,8 +1089,8 @@ impl Factory {
fn create_plugin_manager(
&self,
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- composer: ComposerWeakHandle,
- global_composer: Option<PartialComposerHandle>,
+ composer: FullComposerWeakHandle,
+ global_composer: Option<AnyComposerHandle>,
disable_plugins: DisablePlugins,
) -> PluginManager {
PluginManager::new(io, composer, global_composer, disable_plugins)
@@ -1192,10 +1105,143 @@ impl Factory {
InstallationManager::new(r#loop, io, event_dispatcher)
}
+ /// Builds the common (partial) part of a Composer into `composer`, returning the shared managers
+ /// the full-load branch needs to wire its full-only managers. PHP's PartialComposer constructor
+ /// flow lives here so both the partial and full `Rc::new_cyclic` branches can share it.
+ fn build_composer_base(
+ &self,
+ composer: &mut InnerPartialComposer,
+ composer_weak: AnyComposerWeakHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ config: &std::rc::Rc<std::cell::RefCell<Config>>,
+ full_load: bool,
+ is_global: bool,
+ disable_plugins: DisablePlugins,
+ disable_scripts: bool,
+ local_config_data: &mut IndexMap<String, PhpMixed>,
+ cwd: &str,
+ vendor_dir: &str,
+ ) -> anyhow::Result<ComposerBuildArtifacts> {
+ composer.config = Some(config.clone());
+ if is_global {
+ composer.global = true;
+ }
+
+ if full_load {
+ // load auth configs into the IO instance
+ // TODO(phase-b): load_configuration requires &mut IOInterface; create_composer takes &dyn IOInterface
+ // io.load_configuration(&mut *config.borrow_mut())?;
+
+ // load existing Composer\InstalledVersions instance if available and scripts/plugins are allowed, as they might need it
+ // we only load if the InstalledVersions class wasn't defined yet so that this is only loaded once
+ let installed_versions_path = format!(
+ "{}/composer/installed.php",
+ config.borrow_mut().get_str("vendor-dir")?
+ );
+ if !disable_plugins.is_disabled_at_all()
+ && !disable_scripts
+ && !class_exists("Composer\\InstalledVersions")
+ && file_exists(&installed_versions_path)
+ {
+ // force loading the class at this point so it is loaded from the composer phar and not from the vendor dir
+ // as we cannot guarantee integrity of that file
+ if class_exists("Composer\\InstalledVersions") {
+ FilesystemRepository::safely_load_installed_versions(&installed_versions_path);
+ }
+ }
+ }
+
+ let http_downloader = std::rc::Rc::new(std::cell::RefCell::new(
+ Self::create_http_downloader(io.clone(), config, IndexMap::new())?,
+ ));
+ let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some(
+ io.clone(),
+ ))));
+ let r#loop = std::rc::Rc::new(std::cell::RefCell::new(Loop::new(
+ http_downloader.clone(),
+ Some(process.clone()),
+ )));
+ composer.r#loop = Some(r#loop.clone());
+
+ // initialize event dispatcher with the Composer back-reference
+ let dispatcher = {
+ let mut d = EventDispatcher::new(composer_weak, io.clone(), Some(process.clone()));
+ d.set_run_scripts(!disable_scripts);
+ std::rc::Rc::new(std::cell::RefCell::new(d))
+ };
+ composer.event_dispatcher = Some(dispatcher.clone());
+
+ // initialize repository manager
+ let rm = std::rc::Rc::new(std::cell::RefCell::new(RepositoryFactory::manager(
+ io.clone(),
+ config,
+ Some(http_downloader.clone()),
+ Some(dispatcher.clone()),
+ Some(process.clone()),
+ )?));
+
+ // force-set the version of the global package if not defined as
+ // guessing it adds no value and only takes time
+ if !full_load && !local_config_data.contains_key("version") {
+ local_config_data.insert("version".to_string(), PhpMixed::String("1.0.0".to_string()));
+ }
+
+ // load package
+ let parser = VersionParser::new();
+ let guesser = VersionGuesser::new(
+ config.clone(),
+ process.clone(),
+ parser.clone(),
+ Some(io.clone()),
+ );
+ let mut loader =
+ self.load_root_package(rm.clone(), config.clone(), parser, guesser, io.clone());
+ let package = loader.load(
+ local_config_data
+ .iter()
+ .map(|(k, v)| (k.clone(), Box::new(v.clone())))
+ .collect(),
+ "Composer\\Package\\RootPackage",
+ Some(cwd),
+ )?;
+ // TODO(phase-b): set_package expects RootPackageInterface; loader returns BasePackage
+ // composer.package = Some(package);
+ let _ = package;
+
+ // load local repository
+ self.add_local_repository(
+ io.clone(),
+ &mut rm.borrow_mut(),
+ vendor_dir,
+ composer.package.as_ref().unwrap().clone(),
+ Some(&process),
+ );
+ composer.repository_manager = Some(rm.clone());
+
+ // initialize installation manager
+ let im = std::rc::Rc::new(std::cell::RefCell::new(self.create_installation_manager(
+ r#loop.clone(),
+ io.clone(),
+ Some(dispatcher.clone()),
+ )));
+ composer.installation_manager = Some(im.clone());
+
+ // add installers to the manager (must happen after download manager is created since they read it out of $composer)
+ self.create_default_installers(&im, config, io.clone(), Some(&process));
+
+ Ok(ComposerBuildArtifacts {
+ http_downloader,
+ process,
+ r#loop,
+ dispatcher,
+ im,
+ })
+ }
+
fn create_default_installers(
&self,
im: &std::rc::Rc<std::cell::RefCell<InstallationManager>>,
- composer: &PartialOrFullComposer,
+ config: &std::rc::Rc<std::cell::RefCell<Config>>,
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
process: Option<&std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>,
) {
@@ -1203,21 +1249,15 @@ impl Factory {
process.map(std::rc::Rc::clone),
)));
let bin_dir = trim(
- &composer
- .get_config()
- .borrow_mut()
- .get_str("bin-dir")
- .unwrap_or_default(),
+ &config.borrow_mut().get_str("bin-dir").unwrap_or_default(),
Some("/"),
);
- let bin_compat = composer
- .get_config()
+ let bin_compat = config
.borrow_mut()
.get_str("bin-compat")
.unwrap_or_default();
let vendor_dir = trim(
- &composer
- .get_config()
+ &config
.borrow_mut()
.get_str("vendor-dir")
.unwrap_or_default(),
@@ -1266,7 +1306,7 @@ impl Factory {
config: Option<LocalConfigInput>,
disable_plugins: DisablePlugins,
disable_scripts: bool,
- ) -> anyhow::Result<PartialComposerHandle> {
+ ) -> anyhow::Result<AnyComposerHandle> {
let factory = Self;
// for BC reasons, if a config is passed in either as array or a path that is not the default composer.json path