1
0
mirror of https://github.com/hestiacp/hestiacp.git synced 2025-02-06 09:45:30 +00:00

Quick install cli (#4443)

* Implement CLI install for Quick install

- CLI install for Quick install apps
Install apps: with v-quick-install-app install user domain appname [options]

Notes:
- App name is casesensitive (WordPress instead of Wordpress for example) 
- Option as email="info@hestiacp.com" password="12345678" 
- Does only check if all fields are present if field is not present it will select the default value if available otherwise returns error

Other options:
- apps List all available apps (Usage: v-quick-install-app apps)
- options list all available apps options  (Usage: v-quick-install-app options user domain app)

* Minor changes in comments
This commit is contained in:
Jaap Marcus 2024-07-15 11:51:19 +02:00 committed by GitHub
parent 73bb31a7c2
commit 6159f99356
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 894 additions and 10 deletions

View File

@ -19,6 +19,7 @@
# Bin folder
/bin/v-generate-password-hash
/bin/v-quick-install-app
# Exclude bats submodules if present
/test/test_helper/*

178
bin/v-quick-install-app Executable file
View File

@ -0,0 +1,178 @@
#!/usr/local/hestia/php/bin/php
<?php
//# info: Install Quick Install Web App via CLI
//# options: action [user] [domain] [app] [options ...]
//#
//# example: v-quick-install-app install admin domain.com wordpress email="info@hestiacp" password="123456" username="admin" site_name="HestiaCP Demo" install_directory="/" language="nl_NL" php_version="8.2" database_create="true"
//# example: v-quick-install-app apps
//# example: v-quick-install-app options admin domain.com wordpress
//# Install Quick Install Web App via CLI run v-quick-install-app apps for supported apps.
//# v-quick-install-app options for the app options and v-quick-install-app install to install the app
//# Please note app names are case sensitive
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\ConsoleOutput;
use Hestiacp\quoteshellarg\quoteshellarg;
session_start();
require_once( __DIR__ . '/../web/inc/vendor/autoload.php');
require_once( __DIR__ . '/../web/src/init.php');
define("HESTIA_DIR_BIN", "/usr/local/hestia/bin/");
define("HESTIA_CMD", "/usr/bin/sudo /usr/local/hestia/bin/");
define("DEFAULT_PHP_VERSION", "php-" . exec('php -r "echo substr(phpversion(),0,3);"'));
exec(HESTIA_CMD . "v-list-sys-config json", $output, $return_var);
$data = json_decode(implode("", $output), true);
$sys_arr = $data["config"];
foreach ($sys_arr as $key => $value) {
$_SESSION[$key] = $value;
}
$_SESSION['userContext'] = 'user';
$application = new Application();
$application -> register('install')
->setDescription('Install app via the CLI')
-> addArgument('user', InputArgument::REQUIRED, 'Hestia User')
-> addArgument('domain', InputArgument::REQUIRED, 'Domain')
-> addArgument('app', InputArgument::REQUIRED, 'App Name')
-> addArgument('options', InputArgument::IS_ARRAY, 'Options')
-> setCode(function($input, $output){
$user = $input -> getArgument('user');
$_SESSION['user'] = $user;
$v_domain = $input -> getArgument('domain');
$app = $input -> getArgument('app');
$options = $input -> getArgument('options');
$data = [];
foreach($options as $option){
$o = explode('=', $option);
$data['webapp_'.$o[0]] = $o[1];
}
$hestia = new \Hestia\System\HestiaApp();
if(class_exists("\Hestia\WebApp\Installers\\" . $app . "\\" . $app . "Setup") === false){
$output -> writeln('App not found');
return Command::FAILURE;
}
$app_installer_class = "\Hestia\WebApp\Installers\\" . $app . "\\" . $app . "Setup";
$app_installer = new $app_installer_class($v_domain, $hestia);
// check for default fields
$WebappInstaller = new \Hestia\WebApp\AppWizard($app_installer, $v_domain, $hestia);
$fields = $WebappInstaller -> getOptions();
$array = [];
foreach($fields as $key => $field){
if(is_array($field)){
if(!empty($field['value'])){
$array['webapp_'.$key] = $field['value'];
}
}
}
$data = array_merge($array, $data);
//loop trough data and check all fields are set
$error = false;
foreach($fields as $key => $field){
if(empty($data['webapp_'.$key])){
if(strpos($key, 'database_') !== false){
if($data['webapp_database_create'] != true){
$output -> writeln('Missing required field: ' . $key);
$error = true;
}
}else{
//all ways the case
$output -> writeln('Missing required field: ' . $key);
$error = true;
}
}
}
if($error !== false){
return Command::FAILURE;
}
$installer = new \Hestia\WebApp\AppWizard($app_installer, $v_domain, $hestia);
$installer -> execute($data);
return Command::SUCCESS;
});
$application -> register('apps')
->setDescription('List availble apps')
-> setCode(function($input, $output){
$appInstallers = glob(__DIR__ . "/../web/src/app/WebApp/Installers/*/*.php");
$output -> writeln('Available Apps');
$output -> writeln('---------------------------------');
foreach($appInstallers as $appInstaller){
$app = basename(dirname($appInstaller));
$hestia = new \Hestia\System\HestiaApp();
$domain = 'demo.hestiacp.com';
if( !file_exists(__DIR__ . "/../web/src/app/WebApp/Installers/" . $app . "/". $app . "Setup.php") ){
continue;
}
$app_installer_class = "\Hestia\WebApp\Installers\\" . $app . "\\" . $app . "Setup";
$app_installer = new $app_installer_class($domain, $hestia);
$info = $app_installer -> info();
$output -> writeln($info['name'] . ' - ' . $info['version']);
}
$output -> writeln('---------------------------------');
$output -> writeln('Please note app names are case sensitive');
return Command::SUCCESS;
});
$application -> register('options')
->setDescription('List options requied / optional for the app')
-> addArgument('user', InputArgument::REQUIRED, 'Hestia User')
-> addArgument('domain', InputArgument::REQUIRED, 'Domain')
-> addArgument('app', InputArgument::REQUIRED, 'App Name')
-> setCode(function($input, $output){
$user = $input -> getArgument('user');
$_SESSION['user'] = $user;
$v_domain = $input -> getArgument('domain');
$app = $input -> getArgument('app');
$hestia = new \Hestia\System\HestiaApp();
if(class_exists("\Hestia\WebApp\Installers\\" . $app . "\\" . $app . "Setup") === false){
$output -> writeln('App not found');
return Command::FAILURE;
}
$app_installer_class = "\Hestia\WebApp\Installers\\" . $app . "\\" . $app . "Setup";
$app_installer = new $app_installer_class($v_domain, $hestia);
$WebappInstaller = new \Hestia\WebApp\AppWizard($app_installer, $v_domain, $hestia);
$output -> writeln('To install '.$app.' use the following command:');
$output -> writeln('v-quick-install-app install ' . $user . ' ' . $v_domain . ' ' . $app . ' email="info@hestiacp" password="12346"');
$output -> writeln('---------------------------------');
$output -> writeln('Options for ' . $app);
$output -> writeln('---------------------------------');
$options = $WebappInstaller -> getOptions();
foreach($options as $key => $option){
if(!is_array($option)){
$output -> writeln('Key: ' . $key . ' Type: ' . $option .' (Required)');
}else{
$required = '';
if(empty($option['value'])){
$option['value'] = 'none';
if(strpos($key, 'database_') === false){
$required = '(' . 'Required' . ')';
}
}
if(!empty($option['type'])){
if($option['type'] == 'boolean'){
$option['value'] = $option['value'] ? 'true' : 'false';
}
$output -> writeln('Key: ' .$key . ' Default Value: ' . $option['value'] .' Type: ' . $option['type'] . ' ' . $required);
}else{
$output -> writeln('Key :' .$key . ' Default Value: ' . $option['value'] . ' ' . $required);
}
}
}
return Command::SUCCESS;
});
$application -> run();

View File

@ -24,10 +24,10 @@ class WordpressSetup extends BaseSetup {
//],
"site_name" => ["type" => "text", "value" => "WordPress Blog"],
"wordpress_account_username" => ["value" => "wpadmin"],
"wordpress_account_email" => "text",
"wordpress_account_password" => "password",
"install_directory" => ["type" => "text", "value" => "", "placeholder" => "/"],
"username" => ["value" => "wpadmin"],
"email" => "text",
"password" => "password",
"install_directory" => ["type" => "text", "value" => "/", "placeholder" => "/"],
"language" => [
"type" => "select",
"value" => "en_US",
@ -249,13 +249,13 @@ class WordpressSetup extends BaseSetup {
"weblog_title=" .
rawurlencode($options["site_name"]) .
"&user_name=" .
rawurlencode($options["wordpress_account_username"]) .
rawurlencode($options["username"]) .
"&admin_password=" .
rawurlencode($options["wordpress_account_password"]) .
rawurlencode($options["password"]) .
"&admin_password2=" .
rawurlencode($options["wordpress_account_password"]) .
rawurlencode($options["password"]) .
"&admin_email=" .
rawurlencode($options["wordpress_account_email"]),
rawurlencode($options["email"]),
),
$output,
$return_var,

View File

@ -6,5 +6,8 @@
},
"require-dev": {
"filp/whoops": "2.15.4"
},
"require": {
"symfony/console": "^7.1"
}
}

706
web/src/composer.lock generated
View File

@ -4,8 +4,710 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "44de0539e9881c476d2b164116b8abbb",
"packages": [],
"content-hash": "07bc7884fd965497800d5118a75c4ce3",
"packages": [
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "symfony/console",
"version": "v7.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "0aa29ca177f432ab68533432db0de059f39c92ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/0aa29ca177f432ab68533432db0de059f39c92ae",
"reference": "0aa29ca177f432ab68533432db0de059f39c92ae",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/string": "^6.4|^7.0"
},
"conflict": {
"symfony/dependency-injection": "<6.4",
"symfony/dotenv": "<6.4",
"symfony/event-dispatcher": "<6.4",
"symfony/lock": "<6.4",
"symfony/process": "<6.4"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/lock": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command-line",
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.1.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-28T10:03:55+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's grapheme_* functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"grapheme",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
"reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/string",
"version": "v7.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8",
"reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
"symfony/emoji": "^7.1",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
"symfony/var-exporter": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
"i18n",
"string",
"unicode",
"utf-8",
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.1.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-28T09:27:18+00:00"
}
],
"packages-dev": [
{
"name": "filp/whoops",