diff --git a/composer.json b/composer.json index 0afbfb4319..9e8986d1f7 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "doctrine/cache": "~1.5", "guzzlehttp/guzzle": "^5.3", "guzzlehttp/ringphp": "^1.1", - "platformsh/console-form": ">=0.0.25 <2.0", + "platformsh/console-form": ">=0.0.26 <2.0", "platformsh/client": ">=0.57.0 <2.0", "symfony/console": "^3.0 >=3.2", "symfony/yaml": "^3.0 || ^2.6", @@ -20,7 +20,8 @@ "symfony/dependency-injection": "^3.1", "symfony/config": "^3.1", "paragonie/random_compat": "^2.0", - "ext-json": "*" + "ext-json": "*", + "commerceguys/addressing": "^1.0" }, "suggest": { "drush/drush": "For Drupal projects" diff --git a/composer.lock b/composer.lock index 282fe1a0a0..2ebd59f724 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f7a7490bf13334a9b50e223fc80b021f", + "content-hash": "e057f995dbe2fa6633ef0a0f74e37b92", "packages": [ { "name": "cocur/slugify", @@ -74,6 +74,69 @@ }, "time": "2017-03-23T21:52:55+00:00" }, + { + "name": "commerceguys/addressing", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/commerceguys/addressing.git", + "reference": "8aac9af11d38c1abe04a5e4a252d5a6740b2b69a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/commerceguys/addressing/zipball/8aac9af11d38c1abe04a5e4a252d5a6740b2b69a", + "reference": "8aac9af11d38c1abe04a5e4a252d5a6740b2b69a", + "shasum": "" + }, + "require": { + "doctrine/collections": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "mikey179/vfsstream": "1.*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "2.*", + "symfony/validator": ">=3.2" + }, + "suggest": { + "symfony/validator": "to validate addresses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "CommerceGuys\\Addressing\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bojan Zivanovic" + }, + { + "name": "Damien Tournoud" + } + ], + "description": "Addressing library powered by CLDR and Google's address data.", + "keywords": [ + "address", + "internationalization", + "localization", + "postal" + ], + "support": { + "issues": "https://github.com/commerceguys/addressing/issues", + "source": "https://github.com/commerceguys/addressing/tree/v1.0.6" + }, + "time": "2019-10-21T14:31:17+00:00" + }, { "name": "composer/ca-bundle", "version": "1.3.1", @@ -224,6 +287,75 @@ }, "time": "2017-07-22T12:49:21+00:00" }, + { + "name": "doctrine/collections", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Collections\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Collections Abstraction library", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "array", + "collections", + "iterator" + ], + "support": { + "source": "https://github.com/doctrine/collections/tree/v1.3.0" + }, + "time": "2015-04-14T22:21:58+00:00" + }, { "name": "firebase/php-jwt", "version": "v2.2.0", @@ -778,16 +910,16 @@ }, { "name": "platformsh/console-form", - "version": "v0.0.25", + "version": "v0.0.26", "source": { "type": "git", "url": "https://github.com/platformsh/console-form.git", - "reference": "9b1a93e5e27aa1c1614ac14102a55162f69732ae" + "reference": "39aa4f31c70db66710baeb3fd1123c1e809ac953" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/platformsh/console-form/zipball/9b1a93e5e27aa1c1614ac14102a55162f69732ae", - "reference": "9b1a93e5e27aa1c1614ac14102a55162f69732ae", + "url": "https://api.github.com/repos/platformsh/console-form/zipball/39aa4f31c70db66710baeb3fd1123c1e809ac953", + "reference": "39aa4f31c70db66710baeb3fd1123c1e809ac953", "shasum": "" }, "require": { @@ -815,9 +947,9 @@ "description": "A lightweight Symfony Console form system.", "support": { "issues": "https://github.com/platformsh/console-form/issues", - "source": "https://github.com/platformsh/console-form/tree/v0.0.25" + "source": "https://github.com/platformsh/console-form/tree/v0.0.26" }, - "time": "2021-06-16T16:02:08+00:00" + "time": "2022-01-14T15:24:02+00:00" }, { "name": "psr/container", diff --git a/src/Command/Organization/Billing/OrganizationAddressCommand.php b/src/Command/Organization/Billing/OrganizationAddressCommand.php index ca7f80647f..4603d9bfb2 100644 --- a/src/Command/Organization/Billing/OrganizationAddressCommand.php +++ b/src/Command/Organization/Billing/OrganizationAddressCommand.php @@ -8,9 +8,11 @@ use Platformsh\Cli\Service\Table; use Platformsh\Client\Model\Organization\Address; use Platformsh\Client\Model\Organization\Organization; +use Platformsh\ConsoleForm\Form; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class OrganizationAddressCommand extends OrganizationCommandBase @@ -23,13 +25,19 @@ protected function configure() ->addOrganizationOptions() ->addArgument('property', InputArgument::OPTIONAL, 'The name of a property to view or change') ->addArgument('value', InputArgument::OPTIONAL, 'A new value for the property') - ->addArgument('properties', InputArgument::IS_ARRAY|InputArgument::OPTIONAL, 'Additional property/value pairs'); + ->addArgument('properties', InputArgument::IS_ARRAY|InputArgument::OPTIONAL, 'Additional property/value pairs') + ->addOption('form', null, InputOption::VALUE_NONE, 'Display a form for updating the address interactively'); PropertyFormatter::configureInput($this->getDefinition()); Table::configureInput($this->getDefinition()); } protected function execute(InputInterface $input, OutputInterface $output) { + if ($input->getOption('form') && !$input->isInteractive()) { + $this->stdErr->writeln('The --form option cannot be used non-interactively.'); + return 1; + } + $property = $input->getArgument('property'); $updates = $this->parseUpdates($input); @@ -37,11 +45,28 @@ protected function execute(InputInterface $input, OutputInterface $output) $org = $this->validateOrganizationInput($input, 'orders'); $address = $org->getAddress(); + if ($input->getOption('form')) { + $form = Form::fromArray($this->getAddressFormFields()); + foreach ($address->getProperties() as $key => $value) { + if ($value !== '' && ($field = $form->getField($key))) { + $field->set('default', $value); + } + } + foreach ($updates as $key => $value) { + if ($field = $form->getField($key)) { + $field->set('default', $value); + } + } + /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */ + $questionHelper = $this->getService('question_helper'); + $updates = $form->resolveOptions($input, $output, $questionHelper); + } + /** @var PropertyFormatter $formatter */ $formatter = $this->getService('property_formatter'); $result = 0; - if ($property !== null) { + if ($property !== null || !empty($updates)) { if (empty($updates)) { $formatter->displayData($output, $address->getProperties(), $property); return $result; @@ -76,6 +101,7 @@ protected function display(Address $address, Organization $org, InputInterface $ $table->renderSimple($values, $headings); if (!$table->formatIsMachineReadable()) { + $this->stdErr->writeln(''); $this->stdErr->writeln(\sprintf('To view the billing profile, run: %s', $this->otherCommandExample($input, 'org:billing:profile'))); $this->stdErr->writeln(\sprintf('To view organization details, run: %s', $this->otherCommandExample($input, 'org:info'))); } diff --git a/src/Command/Organization/OrganizationCommandBase.php b/src/Command/Organization/OrganizationCommandBase.php index 6d21d790ee..ff303a22e2 100644 --- a/src/Command/Organization/OrganizationCommandBase.php +++ b/src/Command/Organization/OrganizationCommandBase.php @@ -2,8 +2,16 @@ namespace Platformsh\Cli\Command\Organization; +use CommerceGuys\Addressing\AddressFormat\AddressField; +use CommerceGuys\Addressing\AddressFormat\AddressFormatRepository; +use CommerceGuys\Addressing\AddressFormat\AdministrativeAreaType; +use CommerceGuys\Addressing\AddressFormat\LocalityType; +use CommerceGuys\Addressing\AddressFormat\PostalCodeType; +use CommerceGuys\Addressing\Country\CountryRepository; use Platformsh\Cli\Command\CommandBase; use Platformsh\Client\Model\Organization\Member; +use Platformsh\ConsoleForm\Field\Field; +use Platformsh\ConsoleForm\Field\OptionsField; use Symfony\Component\Console\Input\InputInterface; class OrganizationCommandBase extends CommandBase @@ -52,4 +60,114 @@ protected function otherCommandExample(InputInterface $input, $commandName, $oth } return \implode(' ', $args); } + + /** + * Returns a list of interactive console form fields for an address. + * + * They can dynamically change name, validation or 'required' status, depending on the address country. + * + * @return Field[] + */ + protected function getAddressFormFields() + { + $countryRepository = new CountryRepository(); + $addressFormatRepository = new AddressFormatRepository(); + $countryList = $countryRepository->getList(); + $fields = [ + 'country' => new OptionsField('Country', [ + 'asChoice' => false, + 'includeAsOption' => false, + 'options' => $countryList, + 'normalizer' => function ($value) use ($countryList) { + if (isset($countryList[$value])) { + return $value; + } + return \array_search($value, $countryList, true); + }, + ]), + ]; + + $possibleFields = [ + 'premise' => [AddressField::ADDRESS_LINE1, 'Address line 1 ("premise")'], + 'thoroughfare' => [AddressField::ADDRESS_LINE2, 'Address line 2 ("thoroughfare")'], + 'locality' => [AddressField::LOCALITY, 'City or town ("locality")'], + 'dependent_locality' => [AddressField::DEPENDENT_LOCALITY, 'Dependent locality'], + 'administrative_area' => [AddressField::ADMINISTRATIVE_AREA, 'State/county ("administrative area")'], + 'postal_code' => [AddressField::POSTAL_CODE, 'Postal code'], + ]; + foreach ($possibleFields as $key => $info) { + list($addressFieldName, $name) = $info; + $fields[$key] = new Field($name, [ + 'includeAsOption' => false, + ]); + $field = &$fields[$key]; + $field->set('conditions', [ + 'country' => function ($country) use ($addressFieldName, $addressFormatRepository, $field, $key) { + $format = $addressFormatRepository->get($country); + if (!$format || !\in_array($addressFieldName, $format->getUsedFields())) { + return false; + } + $field->set('required', \in_array($addressFieldName, $format->getRequiredFields(), true)); + if ($addressFieldName === AddressField::LOCALITY) { + if ($localityType = $format->getLocalityType()) { + switch ($localityType) { + case LocalityType::CITY: + $field->set('name', 'City'); + break; + case LocalityType::DISTRICT: + case LocalityType::SUBURB: + $field->set('name', 'District or suburb'); + break; + case LocalityType::POST_TOWN: + $field->set('name', 'City or town'); + break; + } + } + } + if ($addressFieldName === AddressField::ADMINISTRATIVE_AREA) { + $map = [ + AdministrativeAreaType::AREA => 'Area', + AdministrativeAreaType::COUNTY => 'County', + AdministrativeAreaType::DEPARTMENT => 'Department', + AdministrativeAreaType::DISTRICT => 'District', + AdministrativeAreaType::DO_SI => 'Do/Si', + AdministrativeAreaType::EMIRATE => 'Emirate', + AdministrativeAreaType::ISLAND => 'Island', + AdministrativeAreaType::OBLAST => 'Oblast', + AdministrativeAreaType::PARISH => 'Parish', + AdministrativeAreaType::PREFECTURE => 'Prefecture', + AdministrativeAreaType::PROVINCE => 'Province', + AdministrativeAreaType::STATE => 'State', + ]; + if (isset($map[$format->getAdministrativeAreaType()])) { + $field->set('name', $map[$format->getAdministrativeAreaType()]); + } + } + if ($addressFieldName === AddressField::POSTAL_CODE) { + if ($postalCodeType = $format->getPostalCodeType()) { + switch ($postalCodeType) { + case PostalCodeType::PIN: + $field->set('name', 'Pin code'); + break; + case PostalCodeType::EIR: + $field->set('name', 'Eircode'); + break; + case PostalCodeType::ZIP: + $field->set('name', 'Zip code'); + break; + case PostalCodeType::POSTAL: + $field->set('name', 'Postal code'); + break; + } + } + $field->set('validator', function ($value) use ($format) { + return \preg_match('/' . $format->getPostalCodePattern() . '/i', $value) === 1; + }); + } + return true; + }, + ]); + } + return $fields; + } } diff --git a/src/Command/Organization/OrganizationCreateCommand.php b/src/Command/Organization/OrganizationCreateCommand.php index cb779f48e1..0ca71d36b7 100644 --- a/src/Command/Organization/OrganizationCreateCommand.php +++ b/src/Command/Organization/OrganizationCreateCommand.php @@ -106,6 +106,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->runOtherCommand('organization:info', ['--org' => $organization->name], $this->stdErr); + $this->stdErr->writeln(''); + $this->stdErr->writeln(\sprintf('To view or update the organization\'s billing address, run: %s org:billing:address --org %s', $this->config()->get('application.executable'), $organization->name)); + return 0; } }