Add country and zone setup during installation process

This commit is contained in:
Marek Rzytki 2026-06-30 13:17:45 +02:00
parent 9b6799e2b8
commit bc2b6ffa45
12 changed files with 336 additions and 4 deletions

View file

@ -71,6 +71,49 @@
> and their default parameters include Payment Request-specific values (e.g. `hash: paymentRequest.getHash()`).
> Do not use them to configure the Payum after-pay redirect.
## Installer
1. The `sylius:install:setup` command now sets up a **country** and a **default zone** during installation.
Two new setup classes have been introduced:
- `Sylius\Bundle\CoreBundle\Installer\Setup\CountrySetup` implementing `CountrySetupInterface`
- `Sylius\Bundle\CoreBundle\Installer\Setup\ZoneSetup` implementing `ZoneSetupInterface`
Both are registered as services and injected into `SetupCommand`. If you have decorated or replaced
`SetupCommand`, update your constructor to include the two new dependencies:
```diff
public function __construct(
protected readonly EntityManagerInterface $entityManager,
protected readonly CommandDirectoryChecker $commandDirectoryChecker,
protected readonly CurrencySetupInterface $currencySetup,
protected readonly LocaleSetupInterface $localeSetup,
protected readonly ChannelSetupInterface $channelSetup,
+ protected readonly CountrySetupInterface $countrySetup,
+ protected readonly ZoneSetupInterface $zoneSetup,
protected readonly FactoryInterface $adminUserFactory,
protected readonly UserRepositoryInterface $adminUserRepository,
protected readonly ValidatorInterface $validator,
)
```
2. The `ChannelSetupInterface::setup()` method signature has been extended to receive the newly created
country, zone, and console I/O objects:
```diff
public function setup(
LocaleInterface $locale,
CurrencyInterface $currency,
+ CountryInterface $country,
+ ZoneInterface $zone,
+ InputInterface $input,
+ OutputInterface $output,
+ QuestionHelper $questionHelper,
): void;
```
If you have a custom implementation of `ChannelSetupInterface`, update its `setup()` signature accordingly.
## Dependencies
1. The `behat/transliterator` package has been **deprecated** and will be removed in Sylius 3.0.

View file

@ -22,8 +22,10 @@ use Sylius\Bundle\CoreBundle\Console\Command\InstallSampleDataCommand;
use Sylius\Bundle\CoreBundle\Console\Command\SetupCommand;
use Sylius\Bundle\CoreBundle\Installer\Checker\CommandDirectoryChecker;
use Sylius\Bundle\CoreBundle\Installer\Setup\ChannelSetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\CountrySetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\CurrencySetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\LocaleSetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\ZoneSetupInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Console\Application;
@ -44,6 +46,7 @@ final class InstallerContext implements Context
private array $inputChoices = [
'currency' => 'USD',
'locale' => 'en_US',
'country' => 'US',
'e-mail' => 'test@email.com',
'username' => 'test',
'firstName' => '',
@ -59,6 +62,8 @@ final class InstallerContext implements Context
private readonly CurrencySetupInterface $currencySetup,
private readonly LocaleSetupInterface $localeSetup,
private readonly ChannelSetupInterface $channelSetup,
private readonly CountrySetupInterface $countrySetup,
private readonly ZoneSetupInterface $zoneSetup,
private readonly FactoryInterface $adminUserFactory,
private readonly UserRepositoryInterface $adminUserRepository,
private readonly ValidatorInterface $validator,
@ -76,6 +81,8 @@ final class InstallerContext implements Context
$this->currencySetup,
$this->localeSetup,
$this->channelSetup,
$this->countrySetup,
$this->zoneSetup,
$this->adminUserFactory,
$this->adminUserRepository,
$this->validator,

View file

@ -31,6 +31,8 @@ return static function (ContainerConfigurator $container) {
service('sylius.setup.installer.currency'),
service('sylius.setup.installer.locale'),
service('sylius.setup.installer.channel'),
service('sylius.setup.installer.country'),
service('sylius.setup.installer.zone'),
service('sylius.factory.admin_user'),
service('sylius.repository.admin_user'),
service('validator'),

View file

@ -16,8 +16,10 @@ namespace Sylius\Bundle\CoreBundle\Console\Command;
use Doctrine\ORM\EntityManagerInterface;
use Sylius\Bundle\CoreBundle\Installer\Checker\CommandDirectoryChecker;
use Sylius\Bundle\CoreBundle\Installer\Setup\ChannelSetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\CountrySetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\CurrencySetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\LocaleSetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\ZoneSetupInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Sylius\Resource\Factory\FactoryInterface;
@ -49,6 +51,8 @@ final class SetupCommand extends AbstractInstallCommand
protected readonly CurrencySetupInterface $currencySetup,
protected readonly LocaleSetupInterface $localeSetup,
protected readonly ChannelSetupInterface $channelSetup,
protected readonly CountrySetupInterface $countrySetup,
protected readonly ZoneSetupInterface $zoneSetup,
protected readonly FactoryInterface $adminUserFactory,
protected readonly UserRepositoryInterface $adminUserRepository,
protected readonly ValidatorInterface $validator,
@ -74,7 +78,9 @@ EOT
$currency = $this->currencySetup->setup($input, $output, $questionHelper);
$locale = $this->localeSetup->setup($input, $output, $questionHelper);
$this->channelSetup->setup($locale, $currency);
$country = $this->countrySetup->setup($input, $output, $questionHelper);
$zone = $this->zoneSetup->setup($country);
$this->channelSetup->setup($locale, $currency, $country, $zone, $input, $output, $questionHelper);
$this->setupAdministratorUser($input, $output, $locale->getCode());
return Command::SUCCESS;

View file

@ -14,11 +14,17 @@ declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Installer\Setup;
use Doctrine\Persistence\ObjectManager;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Addressing\Model\ZoneInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Currency\Model\CurrencyInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
use Sylius\Resource\Factory\FactoryInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
final class ChannelSetup implements ChannelSetupInterface
{
@ -33,8 +39,15 @@ final class ChannelSetup implements ChannelSetupInterface
) {
}
public function setup(LocaleInterface $locale, CurrencyInterface $currency): void
{
public function setup(
LocaleInterface $locale,
CurrencyInterface $currency,
CountryInterface $country,
ZoneInterface $zone,
InputInterface $input,
OutputInterface $output,
QuestionHelper $questionHelper
): void {
/** @var ChannelInterface|null $channel */
$channel = $this->channelRepository->findOneBy([]);
@ -52,6 +65,12 @@ final class ChannelSetup implements ChannelSetupInterface
$channel->setBaseCurrency($currency);
$channel->addLocale($locale);
$channel->setDefaultLocale($locale);
$channel->addCountry($country);
$question = new ConfirmationQuestion('Assign created zone as default tax zone? (yes/no) [no]: ', false);
if ($questionHelper->ask($input, $output, $question)) {
$channel->setDefaultTaxZone($zone);
}
$this->channelManager->flush();
}

View file

@ -13,10 +13,23 @@ declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Installer\Setup;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Addressing\Model\ZoneInterface;
use Sylius\Component\Currency\Model\CurrencyInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
interface ChannelSetupInterface
{
public function setup(LocaleInterface $locale, CurrencyInterface $currency): void;
public function setup(
LocaleInterface $locale,
CurrencyInterface $currency,
CountryInterface $country,
ZoneInterface $zone,
InputInterface $input,
OutputInterface $output,
QuestionHelper $questionHelper
): void;
}

View file

@ -0,0 +1,94 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Installer\Setup;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
use Sylius\Resource\Factory\FactoryInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Intl\Exception\MissingResourceException;
final class CountrySetup implements CountrySetupInterface
{
/**
* @param RepositoryInterface<CountryInterface> $countryRepository
* @param FactoryInterface<CountryInterface> $countryFactory
*/
public function __construct(
private readonly RepositoryInterface $countryRepository,
private readonly FactoryInterface $countryFactory,
private string $country = 'US',
) {
$this->country = trim($country);
}
public function setup(InputInterface $input, OutputInterface $output, QuestionHelper $questionHelper): CountryInterface
{
$code = $this->getCountryCodeFromUser($input, $output, $questionHelper);
/** @var CountryInterface|null $existingCountry */
$existingCountry = $this->countryRepository->findOneBy(['code' => $code]);
if (null !== $existingCountry) {
return $existingCountry;
}
/** @var CountryInterface $country */
$country = $this->countryFactory->createNew();
$country->setCode($code);
$country->setEnabled(true);
$this->countryRepository->add($country);
return $country;
}
private function getCountryCodeFromUser(InputInterface $input, OutputInterface $output, QuestionHelper $questionHelper): string
{
$code = $this->getNewCountryCode($input, $output, $questionHelper);
$name = $this->getCountryName($code);
while (null === $name) {
$output->writeln(
sprintf('<comment>Country with code <info>%s</info> could not be resolved.</comment>', $code),
);
$code = $this->getNewCountryCode($input, $output, $questionHelper);
$name = $this->getCountryName($code);
}
$output->writeln(sprintf('Adding <info>%s</info> country.', $name));
return $code;
}
private function getNewCountryCode(InputInterface $input, OutputInterface $output, QuestionHelper $questionHelper): string
{
$question = new Question(sprintf('Country (press enter to use %s): ', $this->country), $this->country);
return strtoupper(trim($questionHelper->ask($input, $output, $question)));
}
private function getCountryName(string $code): ?string
{
try {
return Countries::getName($code);
} catch (MissingResourceException) {
return null;
}
}
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Installer\Setup;
use Sylius\Component\Addressing\Model\CountryInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
interface CountrySetupInterface
{
public function setup(InputInterface $input, OutputInterface $output, QuestionHelper $questionHelper): CountryInterface;
}

View file

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Installer\Setup;
use Doctrine\Persistence\ObjectManager;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Addressing\Model\ZoneInterface;
use Sylius\Component\Addressing\Model\ZoneMemberInterface;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
use Sylius\Resource\Factory\FactoryInterface;
final readonly class ZoneSetup implements ZoneSetupInterface
{
/**
* @param RepositoryInterface<ZoneInterface> $zoneRepository
* @param FactoryInterface<ZoneInterface> $zoneFactory
* @param FactoryInterface<ZoneMemberInterface> $zoneMemberFactory
*/
public function __construct(
private RepositoryInterface $zoneRepository,
private FactoryInterface $zoneFactory,
private FactoryInterface $zoneMemberFactory,
private ObjectManager $zoneManager,
) {
}
public function setup(CountryInterface $country): ZoneInterface
{
/** @var ZoneInterface|null $zone */
$zone = $this->zoneRepository->findOneBy([]);
if (null === $zone) {
/** @var ZoneInterface $zone */
$zone = $this->zoneFactory->createNew();
$zone->setCode('default');
$zone->setName('Default');
$zone->setType(ZoneInterface::TYPE_COUNTRY);
$this->zoneManager->persist($zone);
}
if (!$this->zoneHasMemberWithCode($zone, (string) $country->getCode())) {
/** @var ZoneMemberInterface $zoneMember */
$zoneMember = $this->zoneMemberFactory->createNew();
$zoneMember->setCode($country->getCode());
$zone->addMember($zoneMember);
}
$this->zoneManager->flush();
return $zone;
}
private function zoneHasMemberWithCode(ZoneInterface $zone, string $code): bool
{
foreach ($zone->getMembers() as $member) {
if ($member->getCode() === $code) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Installer\Setup;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Addressing\Model\ZoneInterface;
interface ZoneSetupInterface
{
public function setup(CountryInterface $country): ZoneInterface;
}

View file

@ -92,6 +92,8 @@ return static function (ContainerConfigurator $container) {
service('sylius.setup.installer.currency'),
service('sylius.setup.installer.locale'),
service('sylius.setup.installer.channel'),
service('sylius.setup.installer.country'),
service('sylius.setup.installer.zone'),
service('sylius.factory.admin_user'),
service('sylius.repository.admin_user'),
service('validator'),

View file

@ -19,10 +19,14 @@ use Sylius\Bundle\CoreBundle\Installer\Provider\DatabaseSetupCommandsProvider;
use Sylius\Bundle\CoreBundle\Installer\Provider\DatabaseSetupCommandsProviderInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\ChannelSetup;
use Sylius\Bundle\CoreBundle\Installer\Setup\ChannelSetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\CountrySetup;
use Sylius\Bundle\CoreBundle\Installer\Setup\CountrySetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\CurrencySetup;
use Sylius\Bundle\CoreBundle\Installer\Setup\CurrencySetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\LocaleSetup;
use Sylius\Bundle\CoreBundle\Installer\Setup\LocaleSetupInterface;
use Sylius\Bundle\CoreBundle\Installer\Setup\ZoneSetup;
use Sylius\Bundle\CoreBundle\Installer\Setup\ZoneSetupInterface;
return static function (ContainerConfigurator $container) {
$services = $container->services();
@ -76,4 +80,24 @@ return static function (ContainerConfigurator $container) {
])
;
$services->alias(ChannelSetupInterface::class, 'sylius.setup.installer.channel');
$services
->set('sylius.setup.installer.country', CountrySetup::class)
->args([
service('sylius.repository.country'),
service('sylius.factory.country'),
])
;
$services->alias(CountrySetupInterface::class, 'sylius.setup.installer.country');
$services
->set('sylius.setup.installer.zone', ZoneSetup::class)
->args([
service('sylius.repository.zone'),
service('sylius.factory.zone'),
service('sylius.factory.zone_member'),
service('sylius.manager.zone'),
])
;
$services->alias(ZoneSetupInterface::class, 'sylius.setup.installer.zone');
};