//! ref: composer/src/Composer/Command/InitCommand.php use crate::command::PackageDiscoveryTrait; use crate::command::base_command::base_command_initialize; use crate::command::{BaseCommand, BaseCommandData}; use crate::console::input::InputOption; use crate::factory::Factory; use crate::io::IOInterfaceImmutable; use crate::io::io_interface; use crate::json::JsonFile; use crate::json::JsonValidationException; use crate::package::base_package::{self}; use crate::repository::CompositeRepository; use crate::repository::PlatformRepository; use crate::repository::PlatformRepositoryHandle; use crate::repository::RepositoryFactory; use crate::util::Filesystem; use crate::util::ProcessExecutor; use crate::util::Silencer; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::helper::FormatBlockMessages; use shirabe_external_packages::symfony::console::input::ArrayInput; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{ FILE_IGNORE_NEW_LINES, InvalidArgumentException, PHP_EOL, PHP_SERVER, PhpMixed, array_flip_strings, array_intersect_key, array_map, basename, empty, explode, file, file_exists, file_get_contents, file_put_contents, get_current_user, implode, is_dir, is_string, preg_quote, realpath, str_replace, strpos, strtolower, trim, ucwords, }; use shirabe_spdx_licenses::SpdxLicenses; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] pub struct InitCommand { base_command_data: BaseCommandData, /// @var array git_config: std::cell::RefCell>>, repos: std::cell::RefCell>, repository_sets: std::cell::RefCell< IndexMap>>, >, } impl PackageDiscoveryTrait for InitCommand { fn get_repos_mut( &self, ) -> std::cell::RefMut<'_, Option> { self.repos.borrow_mut() } fn get_repository_sets_mut( &self, ) -> std::cell::RefMut< '_, IndexMap>>, > { self.repository_sets.borrow_mut() } } impl Default for InitCommand { fn default() -> Self { Self::new() } } impl InitCommand { pub fn new() -> Self { let command = InitCommand { base_command_data: BaseCommandData::new(None), git_config: std::cell::RefCell::new(None), repos: std::cell::RefCell::new(None), repository_sets: std::cell::RefCell::new(IndexMap::new()), }; command .configure() .expect("InitCommand::configure uses static, valid metadata"); command } } impl Command for InitCommand { fn configure(&self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_available_package_incl_platform() for `require` / `require-dev` self.set_name("init")?; self.set_description("Creates a basic composer.json file in current directory"); self.set_definition(&[ InputOption::new("name", None, Some(InputOption::VALUE_REQUIRED), "Name of the package", None).unwrap().into(), InputOption::new("description", None, Some(InputOption::VALUE_REQUIRED), "Description of package", None).unwrap().into(), InputOption::new("author", None, Some(InputOption::VALUE_REQUIRED), "Author name of package", None).unwrap().into(), InputOption::new("type", None, Some(InputOption::VALUE_REQUIRED), "Type of package (e.g. library, project, metapackage, composer-plugin)", None).unwrap().into(), InputOption::new("homepage", None, Some(InputOption::VALUE_REQUIRED), "Homepage of package", None).unwrap().into(), InputOption::new("require", None, Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), "Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None).unwrap().into(), InputOption::new("require-dev", None, Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED), "Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None).unwrap().into(), InputOption::new("stability", Some(PhpMixed::String("s".to_string())), Some(InputOption::VALUE_REQUIRED), &format!("Minimum stability (empty or one of: {})", implode(", ", &base_package::STABILITIES.keys().map(|k| k.to_string()).collect::>())), None).unwrap().into(), InputOption::new("license", Some(PhpMixed::String("l".to_string())), Some(InputOption::VALUE_REQUIRED), "License of package", None).unwrap().into(), InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories, either by URL or using JSON arrays", None).unwrap().into(), InputOption::new("autoload", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_REQUIRED), "Add PSR-4 autoload mapping. Maps your package's namespace to the provided directory. (Expects a relative path, e.g. src/)", None).unwrap().into(), ]); self.set_help( "The init command creates a basic composer.json file\n\ in the current directory.\n\ \n\ shirabe init\n\ \n\ Read more at https://getcomposer.org/doc/03-cli.md#init", ); Ok(()) } /// @throws \Seld\JsonLint\ParsingException fn execute( &self, input: Rc>, output: Rc>, ) -> anyhow::Result { let io = self.get_io(); let allowlist: Vec = vec![ "name".to_string(), "description".to_string(), "author".to_string(), "type".to_string(), "homepage".to_string(), "require".to_string(), "require-dev".to_string(), "stability".to_string(), "license".to_string(), "autoload".to_string(), ]; let filtered_input: IndexMap = array_intersect_key( &input.borrow().get_options(), &array_flip_strings(&allowlist), ) .into_iter() .collect(); let mut options = shirabe_php_shim::array_filter_map(&filtered_input, |val: &PhpMixed| { !matches!(val, PhpMixed::Null) && !matches!(val, PhpMixed::List(l) if l.is_empty()) }); if options.contains_key("name") && !Preg::is_match( r"{^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$}D", options .get("name") .and_then(|v| v.as_string()) .unwrap_or(""), ) { return Err(InvalidArgumentException { message: format!( "The package name {} is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+", options.get("name").and_then(|v| v.as_string()).unwrap_or("") ), code: 0, } .into()); } if options.contains_key("author") { let author = options .get("author") .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); options.insert( "authors".to_string(), PhpMixed::List( self.format_authors(&author)? .into_iter() .map(|m| PhpMixed::Array(m.into_iter().collect())) .collect(), ), ); options.shift_remove("author"); } let repositories: Vec = input .borrow() .get_option("repository")? .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); if (repositories.len() as i64) > 0 { let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config( Some(io.clone()), None, )?)); for repo in &repositories { let repo_config = RepositoryFactory::config_from_string(io.clone(), &config, repo, true)?; let entry = options .entry("repositories".to_string()) .or_insert_with(|| PhpMixed::List(vec![])); if let PhpMixed::List(list) = entry { list.push(PhpMixed::Array(repo_config.into_iter().collect())); } } } if options.contains_key("stability") { let stab = options.shift_remove("stability").unwrap_or(PhpMixed::Null); options.insert("minimum-stability".to_string(), stab); } let require_value = if options.contains_key("require") { let req_list: Vec = options .get("require") .and_then(|v| v.as_list()) .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let formatted = self.format_requirements(req_list)?; if formatted.is_empty() { // PHP: new \stdClass — empty JSON object PhpMixed::Object(IndexMap::new()) } else { PhpMixed::Array( formatted .into_iter() .map(|(k, v)| (k, PhpMixed::String(v))) .collect(), ) } } else { // PHP: new \stdClass PhpMixed::Object(IndexMap::new()) }; options.insert("require".to_string(), require_value); if options.contains_key("require-dev") { let req_list: Vec = options .get("require-dev") .and_then(|v| v.as_list()) .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let formatted = self.format_requirements(req_list)?; let value = if formatted.is_empty() { PhpMixed::Object(IndexMap::new()) } else { PhpMixed::Array( formatted .into_iter() .map(|(k, v)| (k, PhpMixed::String(v))) .collect(), ) }; options.insert("require-dev".to_string(), value); } // --autoload - create autoload object let mut autoload_path: Option = None; if options.contains_key("autoload") { let ap = options .get("autoload") .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); autoload_path = Some(ap.clone()); let name = input .borrow() .get_option("name")? .as_string() .unwrap_or("") .to_string(); let namespace = self.namespace_from_package_name(&name).unwrap_or_default(); let mut psr4 = IndexMap::new(); psr4.insert(format!("{}\\", namespace), PhpMixed::String(ap)); let mut autoload_obj = IndexMap::new(); autoload_obj.insert("psr-4".to_string(), PhpMixed::Array(psr4)); options.insert("autoload".to_string(), PhpMixed::Array(autoload_obj)); } let file_obj = JsonFile::new(Factory::get_composer_file()?, None, None)?; let options_for_encode: IndexMap = options.clone().into_iter().collect(); let json = JsonFile::encode(&PhpMixed::Array(options_for_encode.clone())); if input.borrow().is_interactive() { io.write_error3(&format!("\n{}\n", json), true, io_interface::NORMAL); if !io.ask_confirmation( "Do you confirm generation [yes]? ".to_string(), true, ) { io.write_error3("Command aborted", true, io_interface::NORMAL); return Ok(1); } } else { io.write_error3( &format!("Writing {}", file_obj.get_path()), true, io_interface::NORMAL, ); } file_obj.write(PhpMixed::Array(options_for_encode.clone()))?; let validate_result = file_obj.validate_schema(JsonFile::LAX_SCHEMA, None); if let Err(e) = validate_result { // try to downcast to JsonValidationException if let Some(json_err) = e.downcast_ref::() { io.write_error3( "Schema validation error, aborting", true, io_interface::NORMAL, ); let errors = format!( " - {}", implode(&format!("{} - ", PHP_EOL), json_err.get_errors()) ); io.write_error3( &format!("{}:{}{}", json_err.get_message(), PHP_EOL, errors), true, io_interface::NORMAL, ); let path_to_unlink = file_obj.get_path().to_string(); let _ = Silencer::call(|| { shirabe_php_shim::unlink(&path_to_unlink); Ok::<(), anyhow::Error>(()) }); return Ok(1); } return Err(e); } // --autoload - Create src folder if let Some(ref ap) = autoload_path { let mut filesystem = Filesystem::new(None); filesystem.ensure_directory_exists(ap); // dump-autoload only for projects without added dependencies. if !self.has_dependencies(&options) { self.run_dump_autoload_command(output.clone()); } } if input.borrow().is_interactive() && is_dir(".git") { let mut ignore_file = realpath(".gitignore").unwrap_or_default(); if ignore_file.is_empty() { ignore_file = format!("{}/.gitignore", realpath(".").unwrap_or_default()); } if !self.has_vendor_ignore(&ignore_file, "vendor") { let question = "Would you like the vendor directory added to your .gitignore [yes]? ".to_string(); if io.ask_confirmation(question, true) { self.add_vendor_ignore(&ignore_file, "/vendor/"); } } } let question = "Would you like to install dependencies now [yes]? ".to_string(); if input.borrow().is_interactive() && self.has_dependencies(&options) && io.ask_confirmation(question, true) { self.update_dependencies(output); } // --autoload - Show post-install configuration info if autoload_path.is_some() { let name = input .borrow() .get_option("name")? .as_string() .unwrap_or("") .to_string(); let namespace = self.namespace_from_package_name(&name).unwrap_or_default(); io.write_error3( &format!( "PSR-4 autoloading configured. Use \"namespace {};\" in {}", namespace, autoload_path.as_deref().unwrap_or("") ), true, io_interface::NORMAL, ); io.write_error3("Include the Composer autoloader with: require 'vendor/autoload.php';", true, io_interface::NORMAL); } Ok(0) } fn initialize( &self, input: Rc>, output: Rc>, ) -> anyhow::Result<()> { base_command_initialize(self, input.clone(), output.clone())?; if !input.borrow().is_interactive() { if input.borrow().get_option("name")?.is_null() { let name = self.get_default_package_name(); input .borrow_mut() .set_option("name", PhpMixed::from(name)) .expect("name option is defined"); } if input.borrow().get_option("author")?.is_null() { let author = self.get_default_author(); input .borrow_mut() .set_option("author", PhpMixed::from(author)) .expect("author option is defined"); } } Ok(()) } fn interact( &self, input: Rc>, output: Rc>, ) { let _ = (|| -> anyhow::Result<()> { let io = self.get_io(); // @var FormatterHelper $formatter — PHP: $this->getHelperSet()->get('formatter') let formatter = self .get_helper_set() .expect("the init command is registered on an application before interact() runs") .borrow() .get_formatter(); // initialize repos if configured let repositories: Vec = input .borrow() .get_option("repository")? .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); if (repositories.len() as i64) > 0 { let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config( Some(io.clone()), None, )?)); io.borrow_mut() .load_configuration(&mut config.borrow_mut())?; let mut repo_manager = RepositoryFactory::manager(io.clone(), &config, None, None, None)?; let mut repos: Vec = vec![crate::repository::RepositoryInterfaceHandle::new( PlatformRepository::new(vec![], IndexMap::new())?, )]; let mut create_default_packagist_repo = true; for repo in &repositories { let repo_config = RepositoryFactory::config_from_string(io.clone(), &config, repo, true)?; let is_packagist_false = repo_config .get("packagist") .map(|v| v.as_bool() == Some(false)) .unwrap_or(false) && repo_config.len() == 1; let is_packagist_org_false = repo_config .get("packagist.org") .map(|v| v.as_bool() == Some(false)) .unwrap_or(false) && repo_config.len() == 1; if is_packagist_false || is_packagist_org_false { create_default_packagist_repo = false; continue; } repos.push(RepositoryFactory::create_repo( io.clone(), &config, repo_config, Some(&mut repo_manager), )?); } if create_default_packagist_repo { let mut default_config: IndexMap = IndexMap::new(); default_config .insert("type".to_string(), PhpMixed::String("composer".to_string())); default_config.insert( "url".to_string(), PhpMixed::String("https://repo.packagist.org".to_string()), ); repos.push(RepositoryFactory::create_repo( io.clone(), &config, default_config, Some(&mut repo_manager), )?); } *self.get_repos_mut() = Some(crate::repository::RepositoryInterfaceHandle::new( CompositeRepository::new(repos), )); // unset($repos, $config, $repositories); } io.write_error3( &format!( "\n{}\n", formatter.borrow().format_block( FormatBlockMessages::String( "Welcome to the Composer config generator".to_string(), ), "bg=blue;fg=white", true, ) ), true, io_interface::NORMAL, ); // namespace io.write_error3( "\nThis command will guide you through creating your composer.json config.\n", true, io_interface::NORMAL, ); let mut name = input .borrow() .get_option("name")? .as_string() .map(|s| s.to_string()) .unwrap_or_else(|| self.get_default_package_name()); let name_default = name.clone(); let name_for_validate = name.clone(); name = io .ask_and_validate( format!( "Package name (/) [{}]: ", name_default ), Box::new(move |value: PhpMixed| -> anyhow::Result { if value.is_null() { return Ok(PhpMixed::String(name_for_validate.clone())); } if !Preg::is_match( r"{^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$}D", value.as_string().unwrap_or(""), ) { return Err(InvalidArgumentException { message: format!( "The package name {} is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+", value.as_string().unwrap_or("") ), code: 0, } .into()); } Ok(value) }), None, PhpMixed::String(name.clone()), )? .as_string() .unwrap_or("") .to_string(); input .borrow_mut() .set_option("name", PhpMixed::String(name)); let description = input .borrow() .get_option("description")? .as_string() .map(|s| s.to_string()); let description_default = description.clone(); let description = io.ask( format!( "Description [{}]: ", description.clone().unwrap_or_default() ), description_default .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ); input.borrow_mut().set_option("description", description); let author = input .borrow() .get_option("author")? .as_string() .map(|s| s.to_string()) .unwrap_or_else(|| self.get_default_author().unwrap_or_default()); let author_for_validate = author.clone(); let author_default = author.clone(); // PHP: $this->parseAuthorString is called inside a closure. We approximate by binding. let author_value = io.ask_and_validate( format!( "Author [{} n to skip]: ", if is_string(&PhpMixed::String(author.clone())) { format!("{}, ", author) } else { String::new() } ), // PHP: function ($value) use ($self, $author) { ... $author = $self->parseAuthorString($value); ... } // The validator is a `'static` closure and cannot borrow `&self`, but // parse_author_string is stateless (it only delegates to the stateless // is_valid_email), so a fresh InitCommand stands in for `$self` here. Box::new(move |value: PhpMixed| -> anyhow::Result { let value_str = value.as_string().map(|s| s.to_string()); if value_str.as_deref() == Some("n") || value_str.as_deref() == Some("no") { return Ok(PhpMixed::Null); } // PHP: $value = $value ?: $author let value = match &value_str { Some(s) if !s.is_empty() => s.clone(), _ => author_for_validate.clone(), }; let parsed = InitCommand::new().parse_author_string(&value)?; let name = parsed.get("name").cloned().flatten().unwrap_or_default(); match parsed.get("email").cloned().flatten() { None => Ok(PhpMixed::String(name)), Some(email) => Ok(PhpMixed::String(format!("{} <{}>", name, email))), } }), None, PhpMixed::String(author_default), )?; input.borrow_mut().set_option("author", author_value); let minimum_stability = input .borrow() .get_option("stability")? .as_string() .map(|s| s.to_string()); let minimum_stability_default = minimum_stability.clone(); let minimum_stability_for_validate = minimum_stability.clone(); let minimum_stability_value = io.ask_and_validate( format!( "Minimum Stability [{}]: ", minimum_stability.clone().unwrap_or_default() ), Box::new(move |value: PhpMixed| -> anyhow::Result { if value.is_null() { return Ok(minimum_stability_for_validate .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null)); } if !base_package::STABILITIES.contains_key(value.as_string().unwrap_or("")) { return Err(InvalidArgumentException { message: format!( "Invalid minimum stability \"{}\". Must be empty or one of: {}", value.as_string().unwrap_or(""), implode( ", ", &base_package::STABILITIES .keys() .map(|k| k.to_string()) .collect::>() ) ), code: 0, } .into()); } Ok(value) }), None, minimum_stability_default .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), )?; input .borrow_mut() .set_option("stability", minimum_stability_value); let type_val = input.borrow().get_option("type")?; let type_str = type_val.as_string().unwrap_or("").to_string(); let mut type_value = io.ask( format!( "Package Type (e.g. library, project, metapackage, composer-plugin) [{}]: ", type_str ), type_val, ); if type_value.as_string() == Some("") || matches!(type_value, PhpMixed::Bool(false)) { type_value = PhpMixed::Null; } input.borrow_mut().set_option("type", type_value); let mut license = input .borrow() .get_option("license")? .as_string() .map(|s| s.to_string()); if license.is_none() { let default_license = PHP_SERVER .lock() .unwrap() .get("COMPOSER_DEFAULT_LICENSE") .map(|value| value.to_string_lossy().into_owned()); if !empty( &default_license .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { license = default_license; } } let license = io.ask( format!( "License [{}]: ", license.clone().unwrap_or_default() ), license.map(PhpMixed::String).unwrap_or(PhpMixed::Null), ); let spdx = SpdxLicenses::new(); if !license.is_null() && !spdx.validate(license.as_string().unwrap_or("")) && license.as_string() != Some("proprietary") { return Err(InvalidArgumentException { message: format!( "Invalid license provided: {}. Only SPDX license identifiers (https://spdx.org/licenses/) or \"proprietary\" are accepted.", license.as_string().unwrap_or("") ), code: 0, } .into()); } input.borrow_mut().set_option("license", license); io.write_error3("\nDefine your dependencies.\n", true, io_interface::NORMAL); // prepare to resolve dependencies let repos = self.get_repos(); let preferred_stability = if let Some(s) = minimum_stability_default.clone().filter(|s| !s.is_empty()) { s } else { "stable".to_string() }; let platform_repo: Option = if repos.is::() { let borrowed = repos.borrow(); let composite = borrowed .as_any() .downcast_ref::() .expect("is::() checked above"); composite .get_repositories() .iter() .find(|candidate| candidate.is::()) .and_then(|candidate| candidate.as_platform_repository()) } else { None }; let question = "Would you like to define your dependencies (require) interactively [yes]? ".to_string(); let require: Vec = input .borrow() .get_option("require")? .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let requirements = if (require.len() as i64) > 0 || io.ask_confirmation(question, true) { self.determine_requirements( input.clone(), output.clone(), require, platform_repo.as_ref(), &preferred_stability, false, false, )? } else { vec![] }; input.borrow_mut().set_option( "require", PhpMixed::List(requirements.into_iter().map(PhpMixed::String).collect()), ); let question = "Would you like to define your dev dependencies (require-dev) interactively [yes]? ".to_string(); let require_dev: Vec = input .borrow() .get_option("require-dev")? .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let dev_requirements = if (require_dev.len() as i64) > 0 || io.ask_confirmation(question, true) { self.determine_requirements( input.clone(), output.clone(), require_dev, platform_repo.as_ref(), &preferred_stability, false, false, )? } else { vec![] }; input.borrow_mut().set_option( "require-dev", PhpMixed::List(dev_requirements.into_iter().map(PhpMixed::String).collect()), ); // --autoload - input and validation let autoload = input .borrow() .get_option("autoload")? .as_string() .map(|s| s.to_string()) .filter(|s| !s.is_empty()) .unwrap_or_else(|| "src/".to_string()); let name_str = input .borrow() .get_option("name")? .as_string() .unwrap_or("") .to_string(); let namespace = self .namespace_from_package_name(&name_str) .unwrap_or_default(); let autoload_for_validate = autoload.clone(); let autoload_default = autoload.clone(); let autoload_value = io.ask_and_validate( format!( "Add PSR-4 autoload mapping? Maps namespace \"{}\" to the entered relative path. [{}, n to skip]: ", namespace, autoload ), Box::new(move |value: PhpMixed| -> anyhow::Result { if value.is_null() { return Ok(PhpMixed::String(autoload_for_validate.clone())); } let value_str = value.as_string().unwrap_or("").to_string(); if value_str == "n" || value_str == "no" { return Ok(PhpMixed::Null); } let value_or_default = if value_str.is_empty() { autoload_for_validate.clone() } else { value_str }; if !Preg::is_match(r"{^[^/][A-Za-z0-9\-_/]+/$}", &value_or_default) { return Err(InvalidArgumentException { message: format!( "The src folder name \"{}\" is invalid. Please add a relative path with tailing forward slash. [A-Za-z0-9_-/]+/", value_or_default.clone(), ), code: 0, } .into()); } Ok(PhpMixed::String(value_or_default)) }), None, PhpMixed::String(autoload_default), )?; input.borrow_mut().set_option("autoload", autoload_value); Ok(()) })(); } shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } impl BaseCommand for InitCommand { fn command_data( &self, ) -> &shirabe_external_packages::symfony::console::command::command::CommandData { self.base_command_data.command_data() } crate::delegate_base_command_trait_impls_to_inner!(base_command_data); } impl InitCommand { /// @return array{name: string, email: string|null} fn parse_author_string( &self, author: &str, ) -> anyhow::Result>> { let mut m: IndexMap = IndexMap::new(); if Preg::is_match3( r#"/^(?P[- .,\p{L}\p{N}\p{Mn}\'’\"()]+)(?:\s+<(?P.+?)>)?$/u"#, author, Some(&mut m), ) { let email = m.get(&CaptureKey::ByName("email".to_string())).cloned(); if let Some(ref email) = email && !self.is_valid_email(email) { return Err(InvalidArgumentException { message: format!("Invalid email \"{}\"", email), code: 0, } .into()); } let mut result: IndexMap> = IndexMap::new(); result.insert( "name".to_string(), Some(trim( &m.get(&CaptureKey::ByName("name".to_string())) .cloned() .unwrap_or_default(), None, )), ); result.insert("email".to_string(), email); return Ok(result); } Err(InvalidArgumentException { message: "Invalid author string. Must be in the formats: Jane Doe or John Smith " .to_string(), code: 0, } .into()) } /// @return array pub(crate) fn format_authors( &self, author: &str, ) -> anyhow::Result>> { let parsed = self.parse_author_string(author)?; let mut author_map: IndexMap = IndexMap::new(); let name = parsed.get("name").cloned().unwrap_or(None); let email = parsed.get("email").cloned().unwrap_or(None); if let Some(name) = name { author_map.insert("name".to_string(), PhpMixed::String(name)); } if let Some(email) = email { author_map.insert("email".to_string(), PhpMixed::String(email)); } Ok(vec![author_map]) } /// Extract namespace from package's vendor name. /// /// new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName" pub fn namespace_from_package_name(&self, package_name: &str) -> Option { if package_name.is_empty() || strpos(package_name, "/").is_none() { return None; } let namespace: Vec = array_map( |part: &String| { let part = Preg::replace(r"/[^a-z0-9]/i", " ", part); let part = ucwords(&part); str_replace(" ", "", &part) }, &explode("/", package_name), ); Some(implode("\\", &namespace)) } /// @return array pub(crate) fn get_git_config(&self) -> IndexMap { if self.git_config.borrow().is_some() { return self.git_config.borrow().clone().unwrap_or_default(); } let mut process = ProcessExecutor::new(Some(self.get_io().clone())); let mut output = String::new(); if process.execute_args( &["git".to_string(), "config".to_string(), "-l".to_string()], &mut output, None, ) == 0 { *self.git_config.borrow_mut() = Some(IndexMap::new()); let mut m: IndexMap> = IndexMap::new(); if Preg::is_match_all3(r"{^([^=]+)=(.*)$}m", &output, Some(&mut m)) { let keys: Vec = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); let values: Vec = m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(); for (key, value) in keys.iter().zip(values.iter()) { self.git_config .borrow_mut() .as_mut() .unwrap() .insert(key.clone(), value.clone()); } } return self.git_config.borrow().clone().unwrap_or_default(); } *self.git_config.borrow_mut() = Some(IndexMap::new()); IndexMap::new() } /// Checks the local .gitignore file for the Composer vendor directory. /// /// Tested patterns include: /// "/$vendor" /// "$vendor" /// "$vendor/" /// "/$vendor/" /// "/$vendor/*" /// "$vendor/*" pub(crate) fn has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { if !file_exists(ignore_file) { return false; } let pattern = format!("{{^/?{}(/\\*?)?$}}", preg_quote(vendor, None)); let lines = file(ignore_file, FILE_IGNORE_NEW_LINES).unwrap_or_default(); for line in &lines { if Preg::is_match(&pattern, line) { return true; } } false } pub(crate) fn add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { let mut contents = String::new(); if file_exists(ignore_file) { contents = file_get_contents(ignore_file).unwrap_or_default(); if strpos(&contents, "\n") != Some(0) { contents.push('\n'); } } file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes()); } /// For testing only: invoke the private `parse_author_string`. pub fn __parse_author_string( &self, author: &str, ) -> anyhow::Result>> { self.parse_author_string(author) } /// For testing only: invoke the crate-private `format_authors`. pub fn __format_authors( &self, author: &str, ) -> anyhow::Result>> { self.format_authors(author) } /// For testing only: invoke the crate-private `get_git_config`. pub fn __get_git_config(&self) -> IndexMap { self.get_git_config() } /// For testing only: invoke the crate-private `has_vendor_ignore`. pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { self.has_vendor_ignore(ignore_file, vendor) } /// For testing only: invoke the crate-private `add_vendor_ignore`. pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { self.add_vendor_ignore(ignore_file, vendor) } pub(crate) fn is_valid_email(&self, email: &str) -> bool { shirabe_php_shim::filter_var_email(email) } fn update_dependencies(&self, output: std::rc::Rc>) { let result = (|| -> anyhow::Result { let application = self .get_application() .expect("a Composer command's application is always set"); let update_command = application.borrow_mut().find("update")?; self.reset_composer()?; let input: std::rc::Rc> = std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); let command = update_command.borrow(); command.run(input, output) })(); if result.is_err() { self.get_io().borrow().write_error( "Could not update dependencies. Run `composer update` to see more information.", ); } } fn run_dump_autoload_command( &self, output: std::rc::Rc>, ) { let result = (|| -> anyhow::Result { let application = self .get_application() .expect("a Composer command's application is always set"); let command = application.borrow_mut().find("dump-autoload")?; self.reset_composer()?; let input: std::rc::Rc> = std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?)); let command = command.borrow(); command.run(input, output) })(); if result.is_err() { self.get_io() .borrow() .write_error("Could not run dump-autoload."); } } /// @param array> $options fn has_dependencies(&self, options: &IndexMap) -> bool { let requires = options.get("require").cloned().unwrap_or(PhpMixed::Null); let requires_arr_empty = match &requires { PhpMixed::Array(m) => m.is_empty(), PhpMixed::List(l) => l.is_empty(), PhpMixed::Null => true, _ => false, }; let dev_requires = options.get("require-dev").cloned(); let dev_requires_arr_empty = match &dev_requires { Some(PhpMixed::Array(m)) => m.is_empty(), Some(PhpMixed::List(l)) => l.is_empty(), Some(PhpMixed::Null) | None => true, _ => false, }; !requires_arr_empty || !dev_requires_arr_empty } fn sanitize_package_name_component(&self, name: &str) -> String { let name = Preg::replace( r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}", "$1$3-$2$4", name, ); let name = strtolower(&name); let name = Preg::replace(r"{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u", "", &name); Preg::replace(r"{([_.-]){2,}}u", "$1", &name) } fn get_default_package_name(&self) -> String { let git = self.get_git_config(); let cwd = realpath(".").unwrap_or_default(); let name = basename(&cwd); let name = self.sanitize_package_name_component(&name); let mut vendor = name.clone(); let composer_default_vendor = PHP_SERVER .lock() .unwrap() .get("COMPOSER_DEFAULT_VENDOR") .map(|value| value.to_string_lossy().into_owned()); let server_username = PHP_SERVER .lock() .unwrap() .get("USERNAME") .map(|value| value.to_string_lossy().into_owned()); let server_user = PHP_SERVER .lock() .unwrap() .get("USER") .map(|value| value.to_string_lossy().into_owned()); if !empty( &composer_default_vendor .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { vendor = composer_default_vendor.unwrap_or_default(); } else if git.contains_key("github.user") { vendor = git.get("github.user").cloned().unwrap_or_default(); } else if !empty( &server_username .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { vendor = server_username.unwrap_or_default(); } else if !empty( &server_user .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { vendor = server_user.unwrap_or_default(); } else if !get_current_user().is_empty() { vendor = get_current_user(); } let vendor = self.sanitize_package_name_component(&vendor); format!("{}/{}", vendor, name) } fn get_default_author(&self) -> Option { let git = self.get_git_config(); let mut author_name: Option = None; let composer_default_author = PHP_SERVER .lock() .unwrap() .get("COMPOSER_DEFAULT_AUTHOR") .map(|value| value.to_string_lossy().into_owned()); if !empty( &composer_default_author .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { author_name = composer_default_author; } else if git.contains_key("user.name") { author_name = git.get("user.name").cloned(); } let mut author_email: Option = None; let composer_default_email = PHP_SERVER .lock() .unwrap() .get("COMPOSER_DEFAULT_EMAIL") .map(|value| value.to_string_lossy().into_owned()); if !empty( &composer_default_email .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), ) { author_email = composer_default_email; } else if git.contains_key("user.email") { author_email = git.get("user.email").cloned(); } if let (Some(name), Some(email)) = (author_name, author_email) { return Some(format!("{} <{}>", name, email)); } None } }