├── LICENSE ├── composer-require-checker.json ├── composer.json ├── node_modules ├── phpunit.xml.dist └── src ├── Client ├── Client.php └── ClientInterface.php ├── Command ├── LoadAudiencesCommand.php ├── PushCustomersCommand.php └── PushOrdersCommand.php ├── Controller └── Action │ ├── LoadAudiencesAction.php │ ├── RepushCustomersAction.php │ └── SubscribeToNewsletterAction.php ├── DataGenerator ├── DataGenerator.php ├── DataGeneratorInterface.php ├── OrderDataGenerator.php ├── OrderDataGeneratorInterface.php ├── ProductDataGenerator.php ├── ProductDataGeneratorInterface.php ├── ProductVariantDataGenerator.php ├── ProductVariantDataGeneratorInterface.php ├── StoreDataGenerator.php └── StoreDataGeneratorInterface.php ├── DependencyInjection ├── Configuration.php └── SetonoSyliusMailchimpExtension.php ├── Doctrine └── ORM │ ├── AudienceRepository.php │ ├── CustomerRepositoryTrait.php │ ├── MailchimpAwareRepositoryTrait.php │ └── OrderRepositoryTrait.php ├── EventListener ├── CustomerRegisterSubscriber.php ├── Doctrine │ ├── AddIndexOnMailchimpStateSubscriber.php │ ├── Customer │ │ └── PushCustomerToMailchimp.php │ ├── IncrementMailchimpTriesSubscriber.php │ └── UpdateMailchimpUpdatedAtSubscriber.php └── UpdateStoreSubscriber.php ├── Exception ├── ClientException.php └── ExceptionInterface.php ├── Fixture ├── Factory │ └── MailchimpExampleFactory.php └── MailchimpFixture.php ├── Form ├── Extension │ ├── Channel │ │ └── ChannelTypeExtension.php │ ├── Checkout │ │ ├── AddressTypeExtension.php │ │ └── CompleteTypeExtension.php │ └── Customer │ │ └── CustomerCheckoutGuestTypeExtension.php └── Type │ ├── AudienceType.php │ ├── CustomerNewsletterSubscriptionType.php │ └── SubscribeToNewsletterType.php ├── Loader ├── AudiencesLoader.php └── AudiencesLoaderInterface.php ├── Menu └── AdminMenuBuilder.php ├── Message ├── Command │ ├── CommandInterface.php │ ├── PushCustomer.php │ ├── PushCustomerBatch.php │ ├── PushCustomers.php │ ├── PushOrderBatch.php │ ├── PushOrders.php │ └── RepushCustomers.php └── Handler │ ├── PushCustomerBatchHandler.php │ ├── PushCustomerHandler.php │ ├── PushCustomersHandler.php │ ├── PushOrderBatchHandler.php │ ├── PushOrdersHandler.php │ └── RepushCustomersHandler.php ├── Model ├── Audience.php ├── AudienceInterface.php ├── ChannelInterface.php ├── ChannelTrait.php ├── CustomerInterface.php ├── CustomerTrait.php ├── MailchimpAwareInterface.php ├── MailchimpAwareTrait.php ├── OrderInterface.php └── OrderTrait.php ├── Provider ├── AudienceProvider.php └── AudienceProviderInterface.php ├── Repository ├── AudienceRepositoryInterface.php ├── CustomerRepositoryInterface.php ├── MailchimpAwareRepositoryInterface.php └── OrderRepositoryInterface.php ├── Resources ├── config │ ├── app │ │ └── config.yaml │ ├── doctrine │ │ └── model │ │ │ └── Audience.orm.xml │ ├── grids │ │ └── setono_sylius_mailchimp_admin_audience.yaml │ ├── routing.yaml │ ├── routing │ │ ├── admin.yaml │ │ └── shop.yaml │ ├── routing_non_localized.yaml │ ├── services.xml │ └── services │ │ ├── block_event_listener.xml │ │ ├── client.xml │ │ ├── command.xml │ │ ├── conditional │ │ └── subscribe.xml │ │ ├── controller.xml │ │ ├── data_generator.xml │ │ ├── event_listener.xml │ │ ├── fixture.xml │ │ ├── form.xml │ │ ├── http_client.xml │ │ ├── loader.xml │ │ ├── menu.xml │ │ ├── message.xml │ │ └── provider.xml ├── public │ └── js │ │ └── shop │ │ └── setono-mailchimp-subscribe.js ├── translations │ ├── messages.da.yml │ ├── messages.en.yml │ ├── validators.da.yml │ └── validators.en.yml └── views │ ├── Admin │ ├── Audience │ │ └── _form.html.twig │ ├── Grid │ │ ├── Action │ │ │ ├── index.html.twig │ │ │ ├── load_audiences.html.twig │ │ │ └── repush_customers.html.twig │ │ └── Field │ │ │ ├── array_count.html.twig │ │ │ ├── channel.html.twig │ │ │ ├── collection.html.twig │ │ │ ├── config.html.twig │ │ │ ├── currency.html.twig │ │ │ ├── list.html.twig │ │ │ └── state.html.twig │ └── Macro │ │ └── buttons.html.twig │ └── Shop │ ├── Subscribe │ ├── _form.html.twig │ └── content.html.twig │ ├── _javascripts.html.twig │ └── subscribe.html.twig ├── SetonoSyliusMailchimpPlugin.php └── Workflow └── MailchimpWorkflow.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Setono 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "array", 4 | "bool", 5 | "callable", 6 | "false", 7 | "float", 8 | "int", 9 | "iterable", 10 | "null", 11 | "object", 12 | "parent", 13 | "self", 14 | "static", 15 | "string", 16 | "true", 17 | "void", 18 | "Sylius\\Bundle\\CoreBundle\\Application\\SyliusPluginTrait", 19 | "Sylius\\Bundle\\ChannelBundle\\Form\\Type\\ChannelChoiceType", 20 | "Sylius\\Bundle\\ChannelBundle\\Form\\Type\\ChannelType", 21 | "Sylius\\Bundle\\CoreBundle\\Application\\SyliusPluginTrait", 22 | "Sylius\\Bundle\\CoreBundle\\Fixture\\AbstractResourceFixture", 23 | "Sylius\\Bundle\\CoreBundle\\Fixture\\Factory\\AbstractExampleFactory", 24 | "Sylius\\Bundle\\CoreBundle\\Fixture\\Factory\\ExampleFactoryInterface", 25 | "Sylius\\Bundle\\CoreBundle\\Fixture\\OptionsResolver\\LazyOption", 26 | "Sylius\\Bundle\\CoreBundle\\Form\\Type\\Checkout\\AddressType", 27 | "Sylius\\Bundle\\CoreBundle\\Form\\Type\\Checkout\\CompleteType", 28 | "Sylius\\Bundle\\CoreBundle\\Form\\Type\\Customer\\CustomerCheckoutGuestType", 29 | "Sylius\\Bundle\\UiBundle\\Menu\\Event\\MenuBuilderEvent", 30 | "Sylius\\Component\\Channel\\Context\\ChannelContextInterface", 31 | "Sylius\\Component\\Channel\\Context\\ChannelNotFoundException", 32 | "Sylius\\Component\\Channel\\Model\\ChannelAwareInterface", 33 | "Sylius\\Component\\Channel\\Model\\ChannelInterface", 34 | "Sylius\\Component\\Channel\\Repository\\ChannelRepositoryInterface", 35 | "Sylius\\Component\\Core\\Model\\ChannelInterface", 36 | "Sylius\\Component\\Core\\Model\\CustomerInterface", 37 | "Sylius\\Component\\Core\\Model\\OrderInterface", 38 | "Sylius\\Component\\Core\\Model\\ProductInterface", 39 | "Sylius\\Component\\Core\\Model\\ProductVariantInterface", 40 | "Sylius\\Component\\Core\\OrderCheckoutStates", 41 | "Sylius\\Component\\Core\\Repository\\CustomerRepositoryInterface", 42 | "Sylius\\Component\\Core\\Repository\\OrderRepositoryInterface", 43 | "Sylius\\Component\\Currency\\Converter\\CurrencyConverterInterface" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setono/sylius-mailchimp-plugin", 3 | "type": "sylius-plugin", 4 | "description": "Mailchimp plugin for Sylius.", 5 | "keywords": [ 6 | "sylius", 7 | "sylius-plugin", 8 | "mailchimp" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": ">=7.4", 13 | "ext-json": "*", 14 | "ext-mbstring": "*", 15 | "doctrine/doctrine-bundle": "^1.12.12 || ^2.0", 16 | "doctrine/event-manager": "^1.1", 17 | "doctrine/orm": "^2.7", 18 | "doctrine/persistence": "^1.3 || ^2.0", 19 | "drewm/mailchimp-api": "^2.5", 20 | "fakerphp/faker": "^1.20", 21 | "knplabs/knp-menu": "^3.3", 22 | "psr/log": "^1.1 || ^2.0 || ^3.0", 23 | "setono/doctrine-orm-batcher": "^0.6.2", 24 | "setono/doctrine-orm-batcher-bundle": "^0.3.1", 25 | "sylius/resource-bundle": "^1.6", 26 | "symfony/cache": "^4.4 || ^5.4 || ^6.0", 27 | "symfony/config": "^4.4 || ^5.4 || ^6.0", 28 | "symfony/console": "^4.4 || ^5.4 || ^6.0", 29 | "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0", 30 | "symfony/event-dispatcher": "^4.4 || ^5.4 || ^6.0", 31 | "symfony/form": "^4.4 || ^5.4 || ^6.0", 32 | "symfony/http-foundation": "^4.4 || ^5.4 || ^6.0", 33 | "symfony/lock": "^4.4 || ^5.4 || ^6.0", 34 | "symfony/messenger": "^4.4 || ^5.4 || ^6.0", 35 | "symfony/options-resolver": "^4.4 || ^5.4 || ^6.0", 36 | "symfony/routing": "^4.4 || ^5.4 || ^6.0", 37 | "symfony/translation-contracts": "^1.0 || ^2.0 || ^3.0", 38 | "symfony/validator": "^4.4 || ^5.4 || ^6.0", 39 | "symfony/workflow": "^4.4 || ^5.4 || ^6.0", 40 | "thecodingmachine/safe": "^1.3", 41 | "twig/twig": "^2.15 || ^3.0", 42 | "webmozart/assert": "^1.11" 43 | }, 44 | "require-dev": { 45 | "friendsofsymfony/oauth-server-bundle": "^1.6 || >2.0.0-alpha.0 ^2.0@dev", 46 | "phpspec/phpspec": "^7.2", 47 | "phpunit/phpunit": "^9.5", 48 | "roave/security-advisories": "dev-latest", 49 | "setono/code-quality-pack": "^1.5", 50 | "sylius/sylius": "~1.9.10", 51 | "symfony/debug-bundle": "^4.4 || ^5.4 || ^6.0", 52 | "symfony/dotenv": "^4.4 || ^5.4 || ^6.0", 53 | "symfony/intl": "^4.4 || ^5.4 || ^6.0", 54 | "symfony/web-profiler-bundle": "^4.4 || ^5.4 || ^6.0" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "dealerdirect/phpcodesniffer-composer-installer": false, 60 | "ergebnis/composer-normalize": true, 61 | "phpstan/extension-installer": false, 62 | "symfony/thanks": false 63 | } 64 | }, 65 | "extra": { 66 | "branch-alias": { 67 | "dev-master": "1.0-dev" 68 | } 69 | }, 70 | "autoload": { 71 | "psr-4": { 72 | "Setono\\SyliusMailchimpPlugin\\": "src/" 73 | } 74 | }, 75 | "autoload-dev": { 76 | "psr-4": { 77 | "Tests\\Setono\\SyliusMailchimpPlugin\\": "tests/" 78 | }, 79 | "classmap": [ 80 | "tests/Application/Kernel.php" 81 | ] 82 | }, 83 | "scripts": { 84 | "all": [ 85 | "@checks", 86 | "@tests" 87 | ], 88 | "analyse": "phpstan analyse -c phpstan.neon", 89 | "assets": [ 90 | "@ensure-assets-installed", 91 | "@ensure-assets-compiled" 92 | ], 93 | "behat": [ 94 | "SYMFONY_ENV=test composer ensure-database-created", 95 | "SYMFONY_ENV=test composer ensure-schema-updated", 96 | "SYMFONY_ENV=test composer ensure-env-copied", 97 | "vendor/bin/behat --tags=\"~@javascript\" --no-interaction --format=progress" 98 | ], 99 | "check-style": "vendor/bin/ecs check src/ spec/ tests/", 100 | "checks": [ 101 | "@check-style", 102 | "@analyse" 103 | ], 104 | "ensure-assets-compiled": "[[ -d tests/Application/public/assets ]] || (cd tests/Application && yarn build && composer ensure-env-copied && bin/console assets:install public -e ${SYMFONY_ENV:-'dev'})", 105 | "ensure-assets-installed": "[[ -d tests/Application/node_modules ]] || (cd tests/Application && yarn install)", 106 | "ensure-database-created": "composer ensure-env-copied && (cd tests/Application && bin/console doctrine:database:create --if-not-exists -e ${SYMFONY_ENV:-'dev'})", 107 | "ensure-env-copied": "([[ ${SYMFONY_ENV:-'dev'} == 'dev' ]] && composer ensure-env-dev-copied) || ([[ ${SYMFONY_ENV:-'dev'} == 'test' ]] && composer ensure-env-test-copied) || echo 'Unknown environment ${SYMFONY_ENV}'", 108 | "ensure-env-dev-copied": "(cd tests/Application && ([[ -f .env.dev.local ]] || cp .env .env.dev.local))", 109 | "ensure-env-test-copied": "(cd tests/Application && ([[ -f .env.test.local ]] || cp .env.test .env.test.local))", 110 | "ensure-schema-updated": "composer ensure-env-copied && (cd tests/Application && bin/console doctrine:schema:update --force -e ${SYMFONY_ENV:-'dev'})", 111 | "ensure-vendors-installed": "[[ -f vendor/autoload.php ]] || php -d memory_limit=-1 /usr/local/bin/composer install", 112 | "fix-style": "vendor/bin/ecs check src/ spec/ tests/ --fix", 113 | "fixtures": [ 114 | "@ensure-database-created", 115 | "@ensure-schema-updated", 116 | "(cd tests/Application && bin/console sylius:fixtures:load --no-interaction -e ${SYMFONY_ENV:-'dev'})" 117 | ], 118 | "phpspec": "vendor/bin/phpspec run", 119 | "phpunit": "vendor/bin/phpunit", 120 | "run": [ 121 | "@ensure-env-copied", 122 | "(cd tests/Application && bin/console server:run -d public -e ${SYMFONY_ENV:-'dev'})" 123 | ], 124 | "tests": [ 125 | "@phpspec", 126 | "@behat" 127 | ], 128 | "try": [ 129 | "@ensure-vendors-installed", 130 | "@assets", 131 | "@fixtures", 132 | "@run" 133 | ] 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /node_modules: -------------------------------------------------------------------------------- 1 | tests/Application/node_modules -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | src/ 8 | 9 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Client/Client.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient; 48 | $this->storeDataGenerator = $storeDataGenerator; 49 | $this->orderDataGenerator = $orderDataGenerator; 50 | $this->productDataGenerator = $productDataGenerator; 51 | $this->productVariantDataGenerator = $productVariantGenerator; 52 | } 53 | 54 | private function makeRequest(string $method, string $uri, array $options = []): array 55 | { 56 | $callable = [$this->httpClient, $method]; 57 | if (!is_callable($callable)) { 58 | throw new RuntimeException(sprintf( 59 | 'The method "%s" does not exist on the http client "%s"', $method, get_class($this->httpClient) 60 | )); 61 | } 62 | $res = $callable($uri, $options); 63 | 64 | if (!$this->httpClient->success()) { 65 | throw new ClientException($uri, $options, $this->httpClient->getLastResponse()); 66 | } 67 | 68 | return $res; 69 | } 70 | 71 | public function getAudiences(array $options = []): array 72 | { 73 | $options = array_merge_recursive([ 74 | 'count' => 1000, 75 | ], $options); 76 | 77 | return $this->makeRequest('get', '/lists', $options)['lists']; 78 | } 79 | 80 | public function updateOrder(OrderInterface $order): void 81 | { 82 | $channel = $order->getChannel(); 83 | Assert::notNull($channel); 84 | 85 | $data = $this->orderDataGenerator->generate($order); 86 | $orderId = $data['id']; 87 | $storeId = $channel->getCode(); 88 | Assert::notNull($storeId); 89 | 90 | $this->ensureProductsExist($channel, $order); 91 | 92 | if ($this->hasOrder($storeId, $orderId)) { 93 | $this->makeRequest('patch', sprintf('/ecommerce/stores/%s/orders/%s', $storeId, $orderId), $data); 94 | } else { 95 | $this->makeRequest('post', sprintf('/ecommerce/stores/%s/orders', $storeId), $data); 96 | } 97 | } 98 | 99 | public function updateStore(AudienceInterface $audience): void 100 | { 101 | $data = $this->storeDataGenerator->generate($audience); 102 | $storeId = $data['id']; 103 | 104 | if ($this->hasStore($storeId)) { 105 | unset($data['id']); 106 | 107 | $this->makeRequest('patch', sprintf('/ecommerce/stores/%s', $storeId), $data); 108 | } else { 109 | $this->makeRequest('post', '/ecommerce/stores', $data); 110 | } 111 | } 112 | 113 | public function updateMember(AudienceInterface $audience, CustomerInterface $customer): void 114 | { 115 | Assert::notNull($customer->getEmail()); 116 | 117 | if (null === $customer->getFirstName() || null === $customer->getLastName()) { 118 | $this->subscribeEmail($audience, $customer->getEmail()); 119 | 120 | return; 121 | } 122 | 123 | $data = [ 124 | 'email_address' => $customer->getEmail(), 125 | 'status' => 'subscribed', 126 | // todo these merge fields are not required to be in mailchimp, so we need to fix this 127 | 'merge_fields' => [ 128 | 'FNAME' => $customer->getFirstName(), 129 | 'LNAME' => $customer->getLastName(), 130 | ], 131 | ]; 132 | 133 | $this->makeRequest('put', 134 | sprintf( 135 | '/lists/%s/members/%s', 136 | $audience->getAudienceId(), 137 | MailChimp::subscriberHash($customer->getEmail()) 138 | ), 139 | $data 140 | ); 141 | } 142 | 143 | public function subscribeEmail(AudienceInterface $audience, string $email): void 144 | { 145 | $data = [ 146 | 'email_address' => $email, 147 | 'status' => 'subscribed', 148 | ]; 149 | 150 | $this->makeRequest('put', 151 | sprintf( 152 | '/lists/%s/members/%s', 153 | $audience->getAudienceId(), 154 | MailChimp::subscriberHash($email) 155 | ), 156 | $data 157 | ); 158 | } 159 | 160 | public function ping(): void 161 | { 162 | $this->makeRequest('get', 'ping'); 163 | } 164 | 165 | private function hasOrder(string $storeId, string $orderId): bool 166 | { 167 | try { 168 | $this->makeRequest('get', sprintf('/ecommerce/stores/%s/orders/%s', $storeId, $orderId)); 169 | 170 | return true; 171 | } catch (ClientException $e) { 172 | if ($e->getStatusCode() === 404) { 173 | return false; 174 | } 175 | 176 | throw $e; 177 | } 178 | } 179 | 180 | private function hasStore(string $storeId): bool 181 | { 182 | try { 183 | $this->makeRequest('get', sprintf('/ecommerce/stores/%s', $storeId)); 184 | 185 | return true; 186 | } catch (ClientException $e) { 187 | if ($e->getStatusCode() === 404) { 188 | return false; 189 | } 190 | 191 | throw $e; 192 | } 193 | } 194 | 195 | private function createProduct(ChannelInterface $channel, ProductInterface $product, ProductVariantInterface $productVariant = null): void 196 | { 197 | $data = $this->productDataGenerator->generate($product, $channel, $productVariant); 198 | 199 | $this->makeRequest('post', sprintf('/ecommerce/stores/%s/products', $channel->getCode()), $data); 200 | } 201 | 202 | private function hasProduct(ChannelInterface $channel, ProductInterface $product): bool 203 | { 204 | try { 205 | $this->makeRequest('get', sprintf('/ecommerce/stores/%s/products/%s', $channel->getCode(), $product->getCode())); 206 | 207 | return true; 208 | } catch (ClientException $e) { 209 | if ($e->getStatusCode() === 404) { 210 | return false; 211 | } 212 | 213 | throw $e; 214 | } 215 | } 216 | 217 | private function createProductVariant(ChannelInterface $channel, ProductVariantInterface $productVariant): void 218 | { 219 | $data = $this->productVariantDataGenerator->generate($productVariant, $channel); 220 | 221 | $product = $productVariant->getProduct(); 222 | Assert::notNull($product); 223 | 224 | $this->makeRequest( 225 | 'post', 226 | sprintf('/ecommerce/stores/%s/products/%s/variants', $channel->getCode(), $product->getCode()), 227 | $data 228 | ); 229 | } 230 | 231 | private function hasProductVariant(ChannelInterface $channel, ProductVariantInterface $productVariant): bool 232 | { 233 | $product = $productVariant->getProduct(); 234 | Assert::notNull($product); 235 | 236 | try { 237 | $this->makeRequest('get', sprintf( 238 | '/ecommerce/stores/%s/products/%s/variants/%s', 239 | $channel->getCode(), $product->getCode(), $productVariant->getCode() 240 | )); 241 | 242 | return true; 243 | } catch (ClientException $e) { 244 | if ($e->getStatusCode() === 404) { 245 | return false; 246 | } 247 | 248 | throw $e; 249 | } 250 | } 251 | 252 | private function ensureProductsExist(ChannelInterface $channel, OrderInterface $order): void 253 | { 254 | foreach ($order->getItems() as $orderItem) { 255 | $variant = $orderItem->getVariant(); 256 | $product = $orderItem->getProduct(); 257 | 258 | if (null === $variant || null === $product) { 259 | continue; 260 | } 261 | 262 | if (!$this->hasProduct($channel, $product)) { 263 | $this->createProduct($channel, $product, $variant); 264 | } 265 | 266 | if (!$this->hasProductVariant($channel, $variant)) { 267 | $this->createProductVariant($channel, $variant); 268 | } 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/Client/ClientInterface.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | $this->audiencesLoader = $audiencesLoader; 33 | } 34 | 35 | protected function configure(): void 36 | { 37 | $this 38 | ->setDescription('Loading audiences from Mailchimp') 39 | ->addOption( 40 | 'preserve', 41 | 'p', 42 | InputOption::VALUE_NONE, 43 | 'Preserve audiences that no longer exists on Mailchimp\'s end' 44 | ) 45 | ; 46 | } 47 | 48 | protected function execute(InputInterface $input, OutputInterface $output): int 49 | { 50 | if (!$this->lock()) { 51 | $output->writeln('The command is already running in another process.'); 52 | 53 | return 0; 54 | } 55 | 56 | $this->client->ping(); 57 | 58 | $preserve = (bool) $input->getOption('preserve'); 59 | $this->audiencesLoader->load($preserve); 60 | 61 | return 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Command/PushCustomersCommand.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | $this->commandBus = $commandBus; 33 | } 34 | 35 | protected function configure(): void 36 | { 37 | $this 38 | ->setDescription('Pushes/synchronizes pending customers to Mailchimp lists. Notice this will not update customers in the ecommerce section of Mailchimp, use setono:sylius-mailchimp:push-orders for that'); 39 | } 40 | 41 | protected function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | if (!$this->lock()) { 44 | $output->writeln('The command is already running in another process.'); 45 | 46 | return 0; 47 | } 48 | 49 | $this->client->ping(); 50 | 51 | $this->commandBus->dispatch(new PushCustomers()); 52 | 53 | $this->release(); 54 | 55 | return 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Command/PushOrdersCommand.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | $this->commandBus = $commandBus; 33 | } 34 | 35 | protected function configure(): void 36 | { 37 | $this 38 | ->setDescription('Pushes/synchronizes pending orders to Mailchimp') 39 | ; 40 | } 41 | 42 | protected function execute(InputInterface $input, OutputInterface $output): int 43 | { 44 | if (!$this->lock()) { 45 | $output->writeln('The command is already running in another process.'); 46 | 47 | return 0; 48 | } 49 | 50 | $this->client->ping(); 51 | 52 | $this->commandBus->dispatch(new PushOrders()); 53 | 54 | $this->release(); 55 | 56 | return 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Controller/Action/LoadAudiencesAction.php: -------------------------------------------------------------------------------- 1 | urlGenerator = $urlGenerator; 29 | $this->audiencesLoader = $audiencesLoader; 30 | } 31 | 32 | public function __invoke(Request $request): RedirectResponse 33 | { 34 | $this->audiencesLoader->load(true); 35 | 36 | return new RedirectResponse($this->urlGenerator->generate('setono_sylius_mailchimp_admin_audience_index')); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Controller/Action/RepushCustomersAction.php: -------------------------------------------------------------------------------- 1 | commandBus = $commandBus; 30 | $this->urlGenerator = $urlGenerator; 31 | } 32 | 33 | public function __invoke(Request $request): RedirectResponse 34 | { 35 | $this->commandBus->dispatch(new RepushCustomers()); 36 | 37 | return new RedirectResponse($this->urlGenerator->generate('setono_sylius_mailchimp_admin_audience_index')); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controller/Action/SubscribeToNewsletterAction.php: -------------------------------------------------------------------------------- 1 | formFactory = $formFactory; 50 | $this->twig = $twig; 51 | $this->translator = $translator; 52 | $this->channelContext = $channelContext; 53 | $this->audienceRepository = $audienceRepository; 54 | $this->client = $client; 55 | } 56 | 57 | public function __invoke(Request $request): Response 58 | { 59 | $form = $this->formFactory->create(SubscribeToNewsletterType::class); 60 | 61 | $form->handleRequest($request); 62 | if ($form->isSubmitted()) { 63 | $audience = $this->getAudience(); 64 | if (null === $audience) { 65 | return $this->json( 66 | $this->translator->trans('setono_sylius_mailchimp.ui.no_audience_associated_with_channel'), 67 | 400 68 | ); 69 | } 70 | 71 | if (!$form->isValid()) { 72 | $errors = $this->getErrorsFromForm($form); 73 | if (is_string($errors)) { 74 | $errors = [$errors]; 75 | } 76 | 77 | return $this->json( 78 | $this->translator->trans('setono_sylius_mailchimp.ui.an_error_occurred'), 400, $errors 79 | ); 80 | } 81 | 82 | $this->client->subscribeEmail($audience, $form->get('email')->getData()); 83 | 84 | return $this->json($this->translator->trans('setono_sylius_mailchimp.ui.subscribed_successfully')); 85 | } 86 | 87 | $template = $request->query->get('template', '@SetonoSyliusMailchimpPlugin/Shop/Subscribe/content.html.twig'); 88 | $content = $this->twig->render($template, [ 89 | 'form' => $form->createView(), 90 | ]); 91 | 92 | return new Response($content); 93 | } 94 | 95 | private function json(string $message, int $status = 200, array $errors = []): JsonResponse 96 | { 97 | return new JsonResponse([ 98 | 'message' => $message, 99 | 'errors' => $errors, 100 | ], $status); 101 | } 102 | 103 | /** 104 | * Taken from https://symfonycasts.com/screencast/javascript/post-proper-api-endpoint#codeblock-99cf6afd45 105 | * 106 | * @return array|string 107 | */ 108 | private function getErrorsFromForm(FormInterface $form) 109 | { 110 | /** @var FormError $error */ 111 | foreach ($form->getErrors() as $error) { 112 | // only supporting 1 error per field 113 | // and not supporting a "field" with errors, that has more 114 | // fields with errors below it 115 | return $error->getMessage(); 116 | } 117 | 118 | $errors = []; 119 | foreach ($form->all() as $childForm) { 120 | $childError = $this->getErrorsFromForm($childForm); 121 | if (is_string($childError)) { 122 | $errors[$childForm->getName()] = $childError; 123 | } 124 | } 125 | 126 | return $errors; 127 | } 128 | 129 | private function getAudience(): ?AudienceInterface 130 | { 131 | $channel = $this->channelContext->getChannel(); 132 | 133 | return $this->audienceRepository->findOneByChannel($channel); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/DataGenerator/DataGenerator.php: -------------------------------------------------------------------------------- 1 | getContext(); 22 | $hostname = $channel->getHostname(); 23 | Assert::notNull($hostname); 24 | $context->setHost($hostname); 25 | 26 | /** 27 | * When we generate URLs we use the default locale since Mailchimp doesn't use translations on stores 28 | * We have chosen the default locale as the translation locale since it makes most sense 29 | */ 30 | $parameters = array_merge([ 31 | '_locale' => self::getDefaultLocaleCode($channel), 32 | ], $parameters); 33 | 34 | return $urlGenerator->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL); 35 | } 36 | 37 | protected static function getDefaultLocaleCode(ChannelInterface $channel): string 38 | { 39 | $locale = $channel->getDefaultLocale(); 40 | if (null === $locale) { 41 | throw new InvalidArgumentException(sprintf('No default locale set for channel %s', $channel->getCode())); 42 | } 43 | 44 | $code = $locale->getCode(); 45 | 46 | if (null === $code) { 47 | throw new InvalidArgumentException(sprintf('No code set for locale with id %s', $locale->getId())); 48 | } 49 | 50 | return $code; 51 | } 52 | 53 | protected static function getBaseCurrencyCode(ChannelInterface $channel): string 54 | { 55 | $currency = $channel->getBaseCurrency(); 56 | if (null === $currency) { 57 | throw new InvalidArgumentException(sprintf('No base currency set for channel %s', $channel->getCode())); 58 | } 59 | 60 | $code = $currency->getCode(); 61 | 62 | if (null === $code) { 63 | throw new InvalidArgumentException(sprintf('No code set for currency with id %s', $currency->getId())); 64 | } 65 | 66 | return $code; 67 | } 68 | 69 | protected static function filterArrayRecursively(array $array): array 70 | { 71 | $res = []; 72 | 73 | foreach ($array as $key => $item) { 74 | if (is_array($item)) { 75 | $val = self::filterArrayRecursively($item); 76 | } else { 77 | $val = $item; 78 | } 79 | 80 | $res[$key] = $val; 81 | } 82 | 83 | return array_filter($res, static function ($elm): bool { 84 | return null !== $elm; 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DataGenerator/DataGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | currencyConverter = $currencyConverter; 23 | } 24 | 25 | public function generate(OrderInterface $order): array 26 | { 27 | /** @var CustomerInterface|null $customer */ 28 | $customer = $order->getCustomer(); 29 | Assert::notNull($customer); 30 | 31 | $shippingAddress = $order->getShippingAddress(); 32 | Assert::notNull($shippingAddress); 33 | 34 | $channel = $order->getChannel(); 35 | Assert::notNull($channel); 36 | 37 | $baseCurrencyCode = self::getBaseCurrencyCode($channel); 38 | $currencyCode = $order->getCurrencyCode(); 39 | Assert::notNull($currencyCode); 40 | 41 | $data = [ 42 | 'id' => $order->getNumber(), 43 | //'campaign_id' => '', // todo 44 | //'landing_site' => '', // todo 45 | 'currency_code' => $baseCurrencyCode, 46 | 'order_total' => $this->convertPrice( 47 | $order->getTotal(), 48 | $currencyCode, 49 | $baseCurrencyCode 50 | ), 51 | //'discount_total' => '', // todo 52 | 'tax_total' => $this->convertPrice($order->getTaxTotal(), $currencyCode, $baseCurrencyCode), 53 | 'shipping_total' => $this->convertPrice($order->getShippingTotal(), $currencyCode, $baseCurrencyCode), 54 | 'customer' => [ 55 | 'id' => (string) $customer->getId(), 56 | 'email_address' => $customer->getEmail(), 57 | 'opt_in_status' => $customer->isSubscribedToNewsletter(), 58 | 'first_name' => $customer->getFirstName(), 59 | 'last_name' => $customer->getLastName(), 60 | 'orders_count' => $customer->getOrders()->count(), 61 | 'address' => [ 62 | 'address1' => $shippingAddress->getStreet(), 63 | 'city' => $shippingAddress->getCity(), 64 | 'province' => $shippingAddress->getProvinceName(), 65 | 'province_code' => $shippingAddress->getProvinceCode(), 66 | 'postal_code' => $shippingAddress->getPostcode(), 67 | 'country_code' => $shippingAddress->getCountryCode(), 68 | ], 69 | ], 70 | 'lines' => [], 71 | ]; 72 | 73 | foreach ($order->getItems() as $orderItem) { 74 | $product = $orderItem->getProduct(); 75 | $variant = $orderItem->getVariant(); 76 | 77 | if (null === $product || null === $variant) { 78 | continue; 79 | } 80 | 81 | $data['lines'][] = [ 82 | 'id' => (string) $orderItem->getId(), 83 | 'product_id' => $product->getCode(), 84 | 'product_variant_id' => $variant->getCode(), 85 | 'quantity' => $orderItem->getQuantity(), 86 | 'price' => $this->convertPrice($orderItem->getTotal(), $currencyCode, $baseCurrencyCode), 87 | ]; 88 | } 89 | 90 | return self::filterArrayRecursively($data); 91 | } 92 | 93 | private function convertPrice(int $amount, string $sourceCurrencyCode, string $targetCurrencyCode): float 94 | { 95 | return round($this->currencyConverter->convert($amount, $sourceCurrencyCode, $targetCurrencyCode) / 100, 2); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/DataGenerator/OrderDataGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | productVariantDataGenerator = $productVariantDataGenerator; 25 | $this->urlGenerator = $urlGenerator; 26 | } 27 | 28 | public function generate( 29 | ProductInterface $product, 30 | ChannelInterface $channel, 31 | ProductVariantInterface $productVariant = null 32 | ): array { 33 | $url = self::generateUrl($this->urlGenerator, $channel, 'sylius_shop_product_show', [ 34 | 'slug' => $product->getSlug(), 35 | ]); 36 | 37 | $data = [ 38 | 'id' => $product->getCode(), 39 | 'title' => $product->getName(), 40 | 'url' => $url, 41 | 'description' => $product->getDescription(), 42 | ]; 43 | 44 | $variants = null === $productVariant ? $product->getVariants() : [$productVariant]; 45 | 46 | /** @var ProductVariantInterface $variant */ 47 | foreach ($variants as $variant) { 48 | $data['variants'][] = $this->productVariantDataGenerator->generate($variant, $channel); 49 | } 50 | 51 | return self::filterArrayRecursively($data); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/DataGenerator/ProductDataGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | urlGenerator = $urlGenerator; 20 | } 21 | 22 | public function generate(ProductVariantInterface $productVariant, ChannelInterface $channel): array 23 | { 24 | $product = $productVariant->getProduct(); 25 | Assert::notNull($product); 26 | 27 | $url = self::generateUrl($this->urlGenerator, $channel, 'sylius_shop_product_show', [ 28 | 'slug' => $product->getSlug(), 29 | ]); 30 | 31 | $data = [ 32 | 'id' => $productVariant->getCode(), 33 | 'title' => $productVariant->getName() ?? $product->getName(), 34 | 'url' => $url, 35 | 'inventory_quantity' => $productVariant->isTracked() ? $productVariant->getOnHand() : null, 36 | 'backorders' => $productVariant->isTracked() ? (string) $productVariant->getOnHold() : null, 37 | // 'price' => '', // todo 38 | // 'image_url' => '', // todo 39 | ]; 40 | 41 | return self::filterArrayRecursively($data); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DataGenerator/ProductVariantDataGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | getChannel(); 18 | 19 | Assert::isInstanceOf($channel, ChannelInterface::class); 20 | 21 | $currencyCode = self::getBaseCurrencyCode($channel); 22 | $localeCode = mb_substr(self::getDefaultLocaleCode($channel), 0, 2); 23 | 24 | $data = [ 25 | 'id' => $channel->getCode(), 26 | 'list_id' => $audience->getAudienceId(), 27 | 'name' => $channel->getName(), 28 | 'platform' => 'Sylius', 29 | 'domain' => $channel->getHostname(), 30 | 'email_address' => $channel->getContactEmail(), 31 | 'currency_code' => $currencyCode, 32 | 'primary_locale' => $localeCode, 33 | ]; 34 | 35 | $shopBillingData = $channel->getShopBillingData(); 36 | if (null !== $shopBillingData) { 37 | $data['address'] = (object) [ 38 | 'address1' => $shopBillingData->getStreet(), 39 | 'city' => $shopBillingData->getCity(), 40 | 'postal_code' => $shopBillingData->getPostcode(), 41 | 'country_code' => $shopBillingData->getCountryCode(), 42 | ]; 43 | 44 | $data['timezone'] = self::getTimeZone($shopBillingData->getCountryCode()); 45 | } 46 | 47 | return self::filterArrayRecursively($data); 48 | } 49 | 50 | /** 51 | * todo This is not the best way to do this, but it works for now 52 | */ 53 | private static function getTimeZone(?string $countryCode): ?string 54 | { 55 | if (null === $countryCode) { 56 | return null; 57 | } 58 | 59 | $identifiers = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $countryCode); 60 | 61 | return count($identifiers) > 0 ? $identifiers[0] : null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/DataGenerator/StoreDataGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 23 | 24 | $rootNode 25 | ->addDefaultsIfNotSet() 26 | ->children() 27 | ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() 28 | ->scalarNode('api_key') 29 | ->isRequired() 30 | ->cannotBeEmpty() 31 | ->info('Your Mailchimp API key') 32 | ->end() 33 | ->booleanNode('subscribe')->defaultTrue()->end() 34 | ->end() 35 | ; 36 | 37 | $this->addResourcesSection($rootNode); 38 | 39 | return $treeBuilder; 40 | } 41 | 42 | private function addResourcesSection(ArrayNodeDefinition $node): void 43 | { 44 | $node 45 | ->children() 46 | ->arrayNode('resources') 47 | ->addDefaultsIfNotSet() 48 | ->children() 49 | ->arrayNode('audience') 50 | ->addDefaultsIfNotSet() 51 | ->children() 52 | ->variableNode('options')->end() 53 | ->arrayNode('classes') 54 | ->addDefaultsIfNotSet() 55 | ->children() 56 | ->scalarNode('model')->defaultValue(Audience::class)->cannotBeEmpty()->end() 57 | ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() 58 | ->scalarNode('repository')->defaultValue(AudienceRepository::class)->cannotBeEmpty()->end() 59 | ->scalarNode('form')->defaultValue(AudienceType::class)->end() 60 | ->scalarNode('factory')->defaultValue(Factory::class)->end() 61 | ->end() 62 | ->end() 63 | ->end() 64 | ->end() 65 | ->end() 66 | ->end() 67 | ->end() 68 | ; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/DependencyInjection/SetonoSyliusMailchimpExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($this->getConfiguration([], $container), $config); 17 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 18 | 19 | $this->registerResources('setono_sylius_mailchimp', $config['driver'], $config['resources'], $container); 20 | 21 | if ($config['subscribe']) { 22 | $loader->load('services/conditional/subscribe.xml'); 23 | } 24 | 25 | $container->setParameter('setono_sylius_mailchimp.api_key', $config['api_key']); 26 | 27 | $loader->load('services.xml'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/AudienceRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('o') 17 | ->andWhere('o.audienceId = :id') 18 | ->setParameter('id', $id) 19 | ->getQuery() 20 | ->getOneOrNullResult() 21 | ; 22 | } 23 | 24 | public function findOneByChannel(ChannelInterface $channel): ?AudienceInterface 25 | { 26 | return $this->createQueryBuilder('o') 27 | ->andWhere('o.channel = :channel') 28 | ->setParameter('channel', $channel) 29 | ->getQuery() 30 | ->getOneOrNullResult() 31 | ; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/CustomerRepositoryTrait.php: -------------------------------------------------------------------------------- 1 | _createPendingPushQueryBuilder($alias); 19 | 20 | return $qb 21 | ->andWhere(sprintf('%s.subscribedToNewsletter = true', $alias)) 22 | ; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/MailchimpAwareRepositoryTrait.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder($alias); 23 | 24 | return $qb 25 | ->andWhere(sprintf('%s.mailchimpState = :state', $alias)) 26 | ->setParameter('state', MailchimpAwareInterface::MAILCHIMP_STATE_PENDING) 27 | ; 28 | } 29 | 30 | public function resetMailchimpState(bool $force = false): void 31 | { 32 | assert($this instanceof EntityRepository); 33 | 34 | $qb = $this->createQueryBuilder('o') 35 | ->update() 36 | ->set('o.mailchimpState', ':state') 37 | ->setParameter('state', MailchimpAwareInterface::MAILCHIMP_STATE_PENDING) 38 | ; 39 | 40 | if (!$force) { 41 | $qb->andWhere('o.mailchimpState = :pushedState') 42 | ->setParameter('pushedState', MailchimpAwareInterface::MAILCHIMP_STATE_PUSHED) 43 | ; 44 | } 45 | 46 | $qb->getQuery()->execute(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Doctrine/ORM/OrderRepositoryTrait.php: -------------------------------------------------------------------------------- 1 | _createPendingPushQueryBuilder($alias); 20 | 21 | return $qb 22 | ->andWhere(sprintf('%s.checkoutState = :checkoutState', $alias)) 23 | ->setParameter('checkoutState', OrderCheckoutStates::STATE_COMPLETED) 24 | ; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/EventListener/CustomerRegisterSubscriber.php: -------------------------------------------------------------------------------- 1 | commandBus = $commandBus; 22 | } 23 | 24 | public static function getSubscribedEvents(): array 25 | { 26 | return [ 27 | 'sylius.customer.post_register' => 'subscribeCustomerToNewsletter', 28 | ]; 29 | } 30 | 31 | public function subscribeCustomerToNewsletter(ResourceControllerEvent $event): void 32 | { 33 | /** @var CustomerInterface|null $customer */ 34 | $customer = $event->getSubject(); 35 | Assert::isInstanceOf($customer, CustomerInterface::class); 36 | 37 | if ($customer->isSubscribedToNewsletter()) { 38 | $this->commandBus->dispatch(new PushCustomer($customer->getId())); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/EventListener/Doctrine/AddIndexOnMailchimpStateSubscriber.php: -------------------------------------------------------------------------------- 1 | mailchimpStateField = $mailchimpStateField; 22 | } 23 | 24 | public function getSubscribedEvents(): array 25 | { 26 | return [Events::loadClassMetadata]; 27 | } 28 | 29 | public function loadClassMetadata(LoadClassMetadataEventArgs $event): void 30 | { 31 | $classMetadata = $event->getClassMetadata(); 32 | $class = $classMetadata->getName(); 33 | if (!is_a($class, MailchimpAwareInterface::class, true)) { 34 | return; 35 | } 36 | 37 | if (!$classMetadata->hasField($this->mailchimpStateField)) { 38 | throw new RuntimeException(sprintf( 39 | 'The class "%s" does not have the field "%s"', $class, $this->mailchimpStateField 40 | )); 41 | } 42 | 43 | $column = $classMetadata->getColumnName($this->mailchimpStateField); 44 | 45 | $classMetadata->table = array_merge_recursive([ 46 | 'indexes' => [ 47 | [ 48 | 'columns' => [ 49 | $column, 50 | ], 51 | ], 52 | ], 53 | ], $classMetadata->table); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/EventListener/Doctrine/Customer/PushCustomerToMailchimp.php: -------------------------------------------------------------------------------- 1 | messageBus = $setonoSyliusMailChimpMessageBus; 21 | } 22 | 23 | public function postPersist(CustomerInterface $customer, LifecycleEventArgs $args): void 24 | { 25 | if (!$customer->isSubscribedToNewsletter()) { 26 | return; 27 | } 28 | 29 | $message = new PushCustomer($customer->getId()); 30 | $this->messageBus->dispatch($message); 31 | } 32 | 33 | public function postUpdate(CustomerInterface $customer, LifecycleEventArgs $args): void 34 | { 35 | $changesSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($customer); 36 | 37 | if (!array_key_exists('subscribedToNewsletter', $changesSet) || false === $changesSet['subscribedToNewsletter'][1]) { 38 | return; 39 | } 40 | 41 | $message = new PushCustomer($customer->getId()); 42 | $this->messageBus->dispatch($message); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/EventListener/Doctrine/IncrementMailchimpTriesSubscriber.php: -------------------------------------------------------------------------------- 1 | mailchimpStateField = $mailchimpStateField; 20 | } 21 | 22 | public function getSubscribedEvents(): array 23 | { 24 | return [Events::preUpdate]; 25 | } 26 | 27 | public function preUpdate(PreUpdateEventArgs $args): void 28 | { 29 | $entity = $args->getObject(); 30 | 31 | if (!$entity instanceof MailchimpAwareInterface) { 32 | return; 33 | } 34 | 35 | if (!$args->hasChangedField($this->mailchimpStateField)) { 36 | return; 37 | } 38 | 39 | // when an entity goes from pending to processing we consider this a try 40 | if ($args->getOldValue($this->mailchimpStateField) !== MailchimpAwareInterface::MAILCHIMP_STATE_PENDING) { 41 | return; 42 | } 43 | 44 | if ($args->getNewValue($this->mailchimpStateField) !== MailchimpAwareInterface::MAILCHIMP_STATE_PROCESSING) { 45 | return; 46 | } 47 | 48 | $entity->incrementMailchimpTries(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/EventListener/Doctrine/UpdateMailchimpUpdatedAtSubscriber.php: -------------------------------------------------------------------------------- 1 | mailchimpStateField = $mailchimpStateField; 21 | } 22 | 23 | public function getSubscribedEvents(): array 24 | { 25 | return [Events::preUpdate]; 26 | } 27 | 28 | public function preUpdate(PreUpdateEventArgs $args): void 29 | { 30 | $entity = $args->getObject(); 31 | 32 | if (!$entity instanceof MailchimpAwareInterface) { 33 | return; 34 | } 35 | 36 | if (!$args->hasChangedField($this->mailchimpStateField)) { 37 | return; 38 | } 39 | 40 | $entity->setMailchimpStateUpdatedAt(new DateTime()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/EventListener/UpdateStoreSubscriber.php: -------------------------------------------------------------------------------- 1 | client = $client; 36 | $this->logger = $logger; 37 | $this->translator = $translator; 38 | } 39 | 40 | public static function getSubscribedEvents(): array 41 | { 42 | return [ 43 | 'setono_sylius_mailchimp.audience.pre_update' => 'update', 44 | ]; 45 | } 46 | 47 | public function update(ResourceControllerEvent $event): void 48 | { 49 | /** @var AudienceInterface|null $audience */ 50 | $audience = $event->getSubject(); 51 | 52 | Assert::isInstanceOf($audience, AudienceInterface::class); 53 | 54 | $channel = $audience->getChannel(); 55 | if (null === $channel) { 56 | return; 57 | } 58 | 59 | try { 60 | $this->client->updateStore($audience); 61 | } catch (ClientException $e) { 62 | $event->stop($this->translator->trans('setono_sylius_mailchimp.ui.channel_association_failed')); 63 | 64 | $this->logger->error(sprintf( 65 | "The user tried to update an audience in Sylius, but got this error: %s\n\nErrors array:\n%s\n\nOptions array:%s", 66 | $e->getMessage(), print_r($e->getErrors(), true), print_r($e->getOptions(), true) 67 | )); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Exception/ClientException.php: -------------------------------------------------------------------------------- 1 | [ 26 | * 'field' => 'field name', 27 | * 'message' => 'error message' 28 | * ] 29 | * ] 30 | * 31 | * @var array 32 | */ 33 | private $errors = []; 34 | 35 | /** 36 | * @param array $lastResponse The response from the Mailchimp HTTP cloent 37 | */ 38 | public function __construct(string $uri, array $options, array $lastResponse) 39 | { 40 | $this->uri = $uri; 41 | $this->options = $options; 42 | $this->parseHeaders($lastResponse); 43 | $message = $this->parseBody($lastResponse); 44 | 45 | parent::__construct($message); 46 | } 47 | 48 | private function parseHeaders(array $response): void 49 | { 50 | if (!isset($response['headers']['http_code'])) { 51 | return; 52 | } 53 | 54 | $this->statusCode = (int) $response['headers']['http_code']; 55 | } 56 | 57 | /** 58 | * @return string The exception message 59 | */ 60 | private function parseBody(array $response): string 61 | { 62 | if (!isset($response['body'])) { 63 | return 'No body on the response.'; 64 | } 65 | 66 | $body = json_decode($response['body'], true); 67 | 68 | if (isset($body['errors']) && is_array($body['errors'])) { 69 | $this->errors = $body['errors']; 70 | } 71 | 72 | return $body['title'] . ': ' . $body['detail']; 73 | } 74 | 75 | public function getUri(): string 76 | { 77 | return $this->uri; 78 | } 79 | 80 | public function getOptions(): array 81 | { 82 | return $this->options; 83 | } 84 | 85 | public function getStatusCode(): int 86 | { 87 | return $this->statusCode; 88 | } 89 | 90 | public function getErrors(): array 91 | { 92 | return $this->errors; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | channelRepository = $channelRepository; 41 | $this->audienceFactory = $audienceFactory; 42 | $this->audienceRepository = $audienceRepository; 43 | 44 | $this->faker = \Faker\Factory::create(); 45 | $this->optionsResolver = new OptionsResolver(); 46 | 47 | $this->configureOptions($this->optionsResolver); 48 | } 49 | 50 | public function create(array $options = []): AudienceInterface 51 | { 52 | return $this->createAudience($options); 53 | } 54 | 55 | protected function createAudience(array $options): AudienceInterface 56 | { 57 | $options = $this->optionsResolver->resolve($options); 58 | 59 | /** @var AudienceInterface|null $audience */ 60 | $audience = $this->audienceRepository->findOneBy(['audienceId' => $options['audience_id']]); 61 | if (null === $audience) { 62 | /** @var AudienceInterface $audience */ 63 | $audience = $this->audienceFactory->createNew(); 64 | } 65 | 66 | $audience->setName($options['name']); 67 | $audience->setAudienceId($options['audience_id']); 68 | $audience->setChannel($options['channel']); 69 | 70 | return $audience; 71 | } 72 | 73 | protected function configureOptions(OptionsResolver $resolver): void 74 | { 75 | $resolver 76 | ->setDefault('name', function (Options $options): string { 77 | /** @var string $text */ 78 | $text = $this->faker->words(3, true); 79 | 80 | return $text; 81 | }) 82 | ->setAllowedTypes('name', 'string') 83 | 84 | ->setDefault('audience_id', function (Options $options): string { 85 | /** @var string $text */ 86 | $text = $this->faker->lexify('??????????'); 87 | 88 | return $text; 89 | }) 90 | ->setAllowedTypes('audience_id', 'string') 91 | 92 | ->setDefault('channel', null) 93 | ->setAllowedTypes('channel', ['null', 'string', ChannelInterface::class]) 94 | ->setNormalizer('channel', LazyOption::findOneBy($this->channelRepository, 'code')) 95 | ; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Fixture/MailchimpFixture.php: -------------------------------------------------------------------------------- 1 | children() 21 | ->scalarNode('name')->cannotBeEmpty()->end() 22 | ->scalarNode('audience_id')->cannotBeEmpty()->end() 23 | ->scalarNode('channel')->cannotBeEmpty()->end() 24 | ; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Form/Extension/Channel/ChannelTypeExtension.php: -------------------------------------------------------------------------------- 1 | add('displaySubscribeToNewsletterAtCheckout', ChoiceType::class, [ 18 | 'label' => 'setono_sylius_mailchimp.form.channel.display_subscribe_to_newsletter_at_checkout', 19 | 'multiple' => true, 20 | 'expanded' => true, 21 | 'required' => false, 22 | 'choices' => ChannelInterface::DISPLAY_NEWSLETTER_SUBSCRIBE_CHOICES, 23 | ]); 24 | } 25 | 26 | public static function getExtendedTypes(): array 27 | { 28 | return [ChannelType::class]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Form/Extension/Checkout/AddressTypeExtension.php: -------------------------------------------------------------------------------- 1 | channelContext = $channelContext; 25 | } 26 | 27 | public function buildForm(FormBuilderInterface $builder, array $options): void 28 | { 29 | /** @var ChannelInterface|null $channel */ 30 | $channel = $this->channelContext->getChannel(); 31 | Assert::isInstanceOf($channel, ChannelInterface::class); 32 | 33 | if (!in_array(ChannelInterface::DISPLAY_NEWSLETTER_SUBSCRIBE_STEP_ADDRESSING, $channel->getDisplaySubscribeToNewsletterAtCheckout() ?? [], true)) { 34 | return; 35 | } 36 | 37 | // Add customer form no matter what to be able to subscribe to newsletter 38 | $builder->add('customer', CustomerCheckoutGuestType::class, ['constraints' => [new Valid()]]); 39 | } 40 | 41 | public static function getExtendedTypes(): array 42 | { 43 | return [AddressType::class]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Form/Extension/Checkout/CompleteTypeExtension.php: -------------------------------------------------------------------------------- 1 | channelContext = $channelContext; 23 | } 24 | 25 | public function buildForm(FormBuilderInterface $builder, array $options): void 26 | { 27 | /** @var ChannelInterface|null $channel */ 28 | $channel = $this->channelContext->getChannel(); 29 | Assert::isInstanceOf($channel, ChannelInterface::class); 30 | 31 | if (!in_array(ChannelInterface::DISPLAY_NEWSLETTER_SUBSCRIBE_STEP_COMPLETE, $channel->getDisplaySubscribeToNewsletterAtCheckout() ?? [], true)) { 32 | return; 33 | } 34 | $builder->add('customer', CustomerNewsletterSubscriptionType::class); 35 | } 36 | 37 | public static function getExtendedTypes(): iterable 38 | { 39 | return [CompleteType::class]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Form/Extension/Customer/CustomerCheckoutGuestTypeExtension.php: -------------------------------------------------------------------------------- 1 | channelContext = $channelContext; 26 | } 27 | 28 | public function buildForm(FormBuilderInterface $builder, array $options): void 29 | { 30 | /** @var ChannelInterface|null $channel */ 31 | $channel = $this->channelContext->getChannel(); 32 | Assert::isInstanceOf($channel, ChannelInterface::class); 33 | 34 | if (!in_array(ChannelInterface::DISPLAY_NEWSLETTER_SUBSCRIBE_STEP_ADDRESSING, $channel->getDisplaySubscribeToNewsletterAtCheckout() ?? [], true)) { 35 | return; 36 | } 37 | 38 | $builder->add('subscribedToNewsletter', CheckboxType::class, [ 39 | 'label' => 'sylius.form.customer.subscribed_to_newsletter', 40 | 'required' => false, 41 | ]); 42 | $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { 43 | $form = $event->getForm(); 44 | /** @var CustomerInterface|null $customer */ 45 | $customer = $event->getData(); 46 | 47 | // If a customer is already existing, remove email field 48 | if (null !== $customer && null !== $customer->getId() && $form->has('email')) { 49 | $form->remove('email'); 50 | } 51 | }); 52 | } 53 | 54 | public static function getExtendedTypes(): array 55 | { 56 | return [CustomerCheckoutGuestType::class]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Form/Type/AudienceType.php: -------------------------------------------------------------------------------- 1 | add('audienceId', TextType::class, [ 18 | 'label' => 'setono_sylius_mailchimp.ui.audience_id', 19 | 'required' => true, 20 | 'disabled' => true, 21 | ]) 22 | ->add('channel', ChannelChoiceType::class, [ 23 | 'label' => 'sylius.ui.channel', 24 | 'required' => false, 25 | 'expanded' => true, 26 | ]) 27 | ; 28 | } 29 | 30 | public function getBlockPrefix(): string 31 | { 32 | return 'setono_sylius_mailchimp_audience'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Form/Type/CustomerNewsletterSubscriptionType.php: -------------------------------------------------------------------------------- 1 | add('subscribedToNewsletter', CheckboxType::class, [ 17 | 'required' => false, 18 | 'label' => 'sylius.form.customer.subscribed_to_newsletter', 19 | ]) 20 | ; 21 | } 22 | 23 | public function getBlockPrefix(): string 24 | { 25 | return 'setono_sylius_mailchimp_customer_newsletter_subscription'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Form/Type/SubscribeToNewsletterType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 19 | 'label' => 'setono_sylius_mailchimp.form.subscribe_to_newsletter.email', 20 | 'constraints' => [ 21 | new NotBlank(), 22 | new Email(), 23 | ], 24 | ]) 25 | ; 26 | } 27 | 28 | public function getBlockPrefix(): string 29 | { 30 | return 'setono_sylius_mailchimp_subscribe_to_newsletter'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Loader/AudiencesLoader.php: -------------------------------------------------------------------------------- 1 | client = $client; 41 | $this->audienceRepository = $audienceRepository; 42 | $this->audienceFactory = $audienceFactory; 43 | $this->audienceManager = $audienceManager; 44 | $this->logger = new NullLogger(); 45 | } 46 | 47 | public function load(bool $preserve = false): void 48 | { 49 | $audienceIds = $this->createOrUpdateAudiences(); 50 | $this->audienceManager->flush(); 51 | 52 | if ($preserve) { 53 | return; 54 | } 55 | 56 | /** @var AudienceInterface[] $audiencesToRemove */ 57 | $audiencesToRemove = array_filter($this->audienceRepository->findAll(), function (AudienceInterface $audience) use ($audienceIds): bool { 58 | return !in_array($audience->getAudienceId(), $audienceIds, true); 59 | }); 60 | 61 | foreach ($audiencesToRemove as $audienceToRemove) { 62 | // @todo Dispatch event to prevent removing? 63 | $this->audienceRepository->remove($audienceToRemove); 64 | 65 | $this->logger->info(sprintf( 66 | 'Audience %s was removed.', 67 | $audienceToRemove->getAudienceId() 68 | )); 69 | } 70 | } 71 | 72 | /** 73 | * @return string[] AudienceIDs of created or updated audiences 74 | */ 75 | protected function createOrUpdateAudiences(): array 76 | { 77 | $mailchimpAudiences = $this->client->getAudiences([ 78 | 'fields' => ['id', 'name'], 79 | ]); 80 | 81 | return array_map(function (array $mailchimpAudience): string { 82 | /** @var string|null $audienceId */ 83 | $audienceId = $mailchimpAudience['id']; 84 | Assert::notNull($audienceId); 85 | 86 | $audience = $this->audienceRepository->findOneByAudienceId($audienceId); 87 | if (null === $audience) { 88 | /** @var AudienceInterface $audience */ 89 | $audience = $this->audienceFactory->createNew(); 90 | $audience->setAudienceId($audienceId); 91 | } 92 | 93 | $audience->setName($mailchimpAudience['name']); 94 | 95 | $this->audienceManager->persist($audience); 96 | 97 | $this->logger->info(sprintf( 98 | 'Audience %s was %s.', 99 | $audienceId, 100 | $audience->getId() !== null ? 'updated' : 'added' 101 | )); 102 | 103 | return $audienceId; 104 | }, $mailchimpAudiences); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Loader/AudiencesLoaderInterface.php: -------------------------------------------------------------------------------- 1 | getHeader($event->getMenu()); 15 | 16 | $header 17 | ->addChild('audiences', [ 18 | 'route' => 'setono_sylius_mailchimp_admin_audience_index', // todo should be the route to audience index 19 | ]) 20 | ->setLabel('setono_sylius_mailchimp.menu.admin.main.mailchimp.audiences') 21 | ->setLabelAttribute('icon', 'users') 22 | ; 23 | } 24 | 25 | private function getHeader(ItemInterface $menu): ItemInterface 26 | { 27 | $header = $menu->getChild('mailchimp'); 28 | if (null !== $header) { 29 | return $header; 30 | } 31 | 32 | $header = $menu->addChild('mailchimp') 33 | ->setLabel('setono_sylius_mailchimp.menu.admin.main.mailchimp.header') 34 | ; 35 | 36 | return $header; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Message/Command/CommandInterface.php: -------------------------------------------------------------------------------- 1 | customerId = $customerId; 15 | } 16 | 17 | public function getCustomerId(): int 18 | { 19 | return $this->customerId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Message/Command/PushCustomerBatch.php: -------------------------------------------------------------------------------- 1 | batch = $batch; 17 | } 18 | 19 | public function getBatch(): BatchInterface 20 | { 21 | return $this->batch; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Message/Command/PushCustomers.php: -------------------------------------------------------------------------------- 1 | batch = $batch; 17 | } 18 | 19 | public function getBatch(): BatchInterface 20 | { 21 | return $this->batch; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Message/Command/PushOrders.php: -------------------------------------------------------------------------------- 1 | queryRebuilder = $queryRebuilder; 55 | $this->client = $client; 56 | $this->managerRegistry = $managerRegistry; 57 | $this->workflowRegistry = $workflowRegistry; 58 | $this->logger = $logger; 59 | $this->audienceProvider = $audienceProvider; 60 | } 61 | 62 | public function __invoke(PushCustomerBatch $message): void 63 | { 64 | $q = $this->queryRebuilder->rebuild($message->getBatch()); 65 | $manager = $this->managerRegistry->getManagerForClass($message->getBatch()->getClass()); 66 | if (null === $manager) { 67 | throw new UnrecoverableMessageHandlingException(sprintf( 68 | 'No object manager available for class %s', $message->getBatch()->getClass() 69 | )); 70 | } 71 | 72 | /** @var CustomerInterface[] $customers */ 73 | $customers = $q->getResult(); 74 | 75 | foreach ($customers as $customer) { 76 | $workflow = $this->getWorkflow($customer); 77 | 78 | if (!$workflow->can($customer, MailchimpWorkflow::TRANSITION_PROCESS)) { 79 | // this means that the state was changed another place 80 | continue; 81 | } 82 | 83 | $workflow->apply($customer, MailchimpWorkflow::TRANSITION_PROCESS); 84 | $manager->flush(); 85 | 86 | try { 87 | $audience = $this->audienceProvider->getAudienceFromCustomerOrders($customer); 88 | if (null === $audience) { 89 | $audience = $this->audienceProvider->getAudienceFromContext(); 90 | } 91 | if (null === $audience) { 92 | // todo maybe this should fire a warning somewhere 93 | continue; 94 | } 95 | 96 | $this->client->updateMember($audience, $customer); 97 | 98 | if (!$workflow->can($customer, MailchimpWorkflow::TRANSITION_PUSH)) { 99 | throw new UnrecoverableMessageHandlingException(sprintf( 100 | 'Could not apply transition "push" on customer with id "%s". Mailchimp state: "%s"', 101 | $customer->getId(), $customer->getMailchimpState() 102 | )); 103 | } 104 | 105 | $workflow->apply($customer, MailchimpWorkflow::TRANSITION_PUSH); 106 | } catch (Throwable $e) { 107 | $this->logger->error(self::buildErrorMessage($e)); 108 | $customer->setMailchimpError(self::buildErrorMessage($e)); 109 | $workflow->apply($customer, MailchimpWorkflow::TRANSITION_FAIL); 110 | } finally { 111 | $manager->flush(); 112 | } 113 | } 114 | } 115 | 116 | private function getWorkflow(object $obj): Workflow 117 | { 118 | if (null === $this->workflow) { 119 | $this->workflow = $this->workflowRegistry->get($obj, MailchimpWorkflow::NAME); // todo use constant here 120 | } 121 | 122 | return $this->workflow; 123 | } 124 | 125 | private static function buildErrorMessage(Throwable $e): string 126 | { 127 | $error = $e->getMessage() . "\n\n"; 128 | if ($e instanceof ClientException) { 129 | $error .= 'Uri: ' . $e->getUri() . "\n\n"; 130 | $error .= 'Status code: ' . $e->getStatusCode() . "\n\n"; 131 | $error .= "Options:\n" . print_r($e->getOptions(), true) . "\n\n"; 132 | } 133 | 134 | $error .= $e->getTraceAsString(); 135 | 136 | return $error; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Message/Handler/PushCustomerHandler.php: -------------------------------------------------------------------------------- 1 | customerRepository = $customerRepository; 51 | $this->audienceProvider = $audienceProvider; 52 | $this->client = $client; 53 | $this->workflowRegistry = $workflowRegistry; 54 | $this->logger = $logger; 55 | $this->managerRegistry = $managerRegistry; 56 | } 57 | 58 | public function __invoke(PushCustomer $message): void 59 | { 60 | /** @var CustomerInterface|null $customer */ 61 | $customer = $this->customerRepository->find($message->getCustomerId()); 62 | Assert::isInstanceOf($customer, CustomerInterface::class); 63 | 64 | $workflow = $this->workflowRegistry->get($customer, MailchimpWorkflow::NAME); 65 | if (!$workflow->can($customer, MailchimpWorkflow::TRANSITION_PROCESS)) { 66 | // this means that the state was changed another place 67 | return; 68 | } 69 | 70 | $workflow->apply($customer, MailchimpWorkflow::TRANSITION_PROCESS); 71 | 72 | $manager = $this->managerRegistry->getManagerForClass(get_class($customer)); 73 | if (null === $manager) { 74 | throw new UnrecoverableMessageHandlingException(sprintf( 75 | 'No object manager available for class %s', get_class($customer) 76 | )); 77 | } 78 | $manager->flush(); 79 | 80 | try { 81 | $audience = $this->audienceProvider->getAudienceFromCustomerOrders($customer); 82 | if (null === $audience) { 83 | $audience = $this->audienceProvider->getAudienceFromContext(); 84 | } 85 | if (null === $audience) { 86 | // todo maybe this should fire a warning somewhere 87 | return; 88 | } 89 | 90 | $this->client->updateMember($audience, $customer); 91 | 92 | if (!$workflow->can($customer, MailchimpWorkflow::TRANSITION_PUSH)) { 93 | throw new UnrecoverableMessageHandlingException(sprintf( 94 | 'Could not apply transition "push" on customer with id "%s". Mailchimp state: "%s"', 95 | $customer->getId(), $customer->getMailchimpState() 96 | )); 97 | } 98 | 99 | $workflow->apply($customer, MailchimpWorkflow::TRANSITION_PUSH); 100 | } catch (\Throwable $e) { 101 | $this->logger->error(self::buildErrorMessage($e)); 102 | $customer->setMailchimpError(self::buildErrorMessage($e)); 103 | $workflow->apply($customer, MailchimpWorkflow::TRANSITION_FAIL); 104 | } 105 | } 106 | 107 | private static function buildErrorMessage(\Throwable $e): string 108 | { 109 | $error = $e->getMessage() . "\n\n"; 110 | if ($e instanceof ClientException) { 111 | $error .= 'Uri: ' . $e->getUri() . "\n\n"; 112 | $error .= 'Status code: ' . $e->getStatusCode() . "\n\n"; 113 | $error .= "Options:\n" . print_r($e->getOptions(), true) . "\n\n"; 114 | } 115 | 116 | $error .= $e->getTraceAsString(); 117 | 118 | return $error; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Message/Handler/PushCustomersHandler.php: -------------------------------------------------------------------------------- 1 | batcherFactory = $batcherFactory; 31 | $this->customerRepository = $customerRepository; 32 | $this->commandBus = $commandBus; 33 | } 34 | 35 | public function __invoke(PushCustomers $message): void 36 | { 37 | $batcher = $this->batcherFactory->createIdCollectionBatcher($this->customerRepository->createMailchimpPendingQueryBuilder()); 38 | foreach ($batcher->getBatches() as $batch) { 39 | $this->commandBus->dispatch(new PushCustomerBatch($batch)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Message/Handler/PushOrderBatchHandler.php: -------------------------------------------------------------------------------- 1 | queryRebuilder = $queryRebuilder; 49 | $this->client = $client; 50 | $this->managerRegistry = $managerRegistry; 51 | $this->workflowRegistry = $workflowRegistry; 52 | $this->logger = $logger; 53 | } 54 | 55 | public function __invoke(PushOrderBatch $message): void 56 | { 57 | $q = $this->queryRebuilder->rebuild($message->getBatch()); 58 | $manager = $this->managerRegistry->getManagerForClass($message->getBatch()->getClass()); 59 | if (null === $manager) { 60 | throw new UnrecoverableMessageHandlingException(sprintf( 61 | 'No object manager available for class %s', $message->getBatch()->getClass() 62 | )); 63 | } 64 | 65 | /** @var OrderInterface[] $orders */ 66 | $orders = $q->getResult(); 67 | 68 | foreach ($orders as $order) { 69 | $workflow = $this->getWorkflow($order); 70 | 71 | // todo use constant 72 | if (!$workflow->can($order, 'process')) { 73 | // this means that the state was changed another place 74 | continue; 75 | } 76 | 77 | $workflow->apply($order, 'process'); // todo use constant 78 | $manager->flush(); 79 | 80 | try { 81 | $this->client->updateOrder($order); 82 | 83 | // todo use constant 84 | if (!$workflow->can($order, 'push')) { 85 | throw new UnrecoverableMessageHandlingException(sprintf( 86 | 'Could not apply transition "push" on order with id "%s". Mailchimp state: "%s"', 87 | $order->getId(), $order->getMailchimpState() 88 | )); 89 | } 90 | 91 | $workflow->apply($order, 'push'); // todo use constant 92 | } catch (Throwable $e) { 93 | $this->logger->error($e->getMessage()); 94 | $order->setMailchimpError(self::buildErrorMessage($e)); 95 | $workflow->apply($order, 'fail'); // todo use constant 96 | } finally { 97 | $manager->flush(); 98 | } 99 | } 100 | } 101 | 102 | private function getWorkflow(object $obj): Workflow 103 | { 104 | if (null === $this->workflow) { 105 | $this->workflow = $this->workflowRegistry->get($obj, 'mailchimp'); // todo use constant here 106 | } 107 | 108 | return $this->workflow; 109 | } 110 | 111 | private static function buildErrorMessage(Throwable $e): string 112 | { 113 | $error = $e->getMessage() . "\n\n"; 114 | if ($e instanceof ClientException) { 115 | $error .= 'Uri: ' . $e->getUri() . "\n\n"; 116 | $error .= 'Status code: ' . $e->getStatusCode() . "\n\n"; 117 | $error .= "Options:\n" . print_r($e->getOptions(), true) . "\n\n"; 118 | } 119 | 120 | $error .= $e->getTraceAsString(); 121 | 122 | return $error; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Message/Handler/PushOrdersHandler.php: -------------------------------------------------------------------------------- 1 | batcherFactory = $batcherFactory; 31 | $this->orderRepository = $orderRepository; 32 | $this->commandBus = $commandBus; 33 | } 34 | 35 | public function __invoke(PushOrders $message): void 36 | { 37 | $batcher = $this->batcherFactory->createIdCollectionBatcher($this->orderRepository->createMailchimpPendingQueryBuilder()); 38 | foreach ($batcher->getBatches() as $batch) { 39 | $this->commandBus->dispatch(new PushOrderBatch($batch)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Message/Handler/RepushCustomersHandler.php: -------------------------------------------------------------------------------- 1 | customerRepository = $customerRepository; 26 | $this->commandBus = $commandBus; 27 | } 28 | 29 | public function __invoke(RepushCustomers $message): void 30 | { 31 | $this->customerRepository->resetMailchimpState(); 32 | 33 | $this->commandBus->dispatch(new PushCustomers()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Model/Audience.php: -------------------------------------------------------------------------------- 1 | id; 26 | } 27 | 28 | public function getName(): ?string 29 | { 30 | return $this->name; 31 | } 32 | 33 | public function setName(?string $name): void 34 | { 35 | $this->name = $name; 36 | } 37 | 38 | public function getAudienceId(): ?string 39 | { 40 | return $this->audienceId; 41 | } 42 | 43 | public function setAudienceId(?string $listId): void 44 | { 45 | $this->audienceId = $listId; 46 | } 47 | 48 | public function getChannel(): ?ChannelInterface 49 | { 50 | return $this->channel; 51 | } 52 | 53 | public function setChannel(?ChannelInterface $channel): void 54 | { 55 | $this->channel = $channel; 56 | } 57 | 58 | public function isCustomerExportable(CustomerInterface $customer): bool 59 | { 60 | return $customer->isSubscribedToNewsletter(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Model/AudienceInterface.php: -------------------------------------------------------------------------------- 1 | self::DISPLAY_NEWSLETTER_SUBSCRIBE_STEP_ADDRESSING, 17 | 'setono_sylius_mailchimp.form.channel.display_newsletter_subscribe.' . self::DISPLAY_NEWSLETTER_SUBSCRIBE_STEP_COMPLETE => self::DISPLAY_NEWSLETTER_SUBSCRIBE_STEP_COMPLETE, 18 | ]; 19 | 20 | public function getDisplaySubscribeToNewsletterAtCheckout(): ?array; 21 | 22 | public function setDisplaySubscribeToNewsletterAtCheckout(?array $displaySubscribeToNewsletterAtCheckout): void; 23 | } 24 | -------------------------------------------------------------------------------- /src/Model/ChannelTrait.php: -------------------------------------------------------------------------------- 1 | displaySubscribeToNewsletterAtCheckout; 22 | } 23 | 24 | public function setDisplaySubscribeToNewsletterAtCheckout(?array $displaySubscribeToNewsletterAtCheckout): void 25 | { 26 | foreach ($displaySubscribeToNewsletterAtCheckout as $key => $value) { 27 | if (!in_array($value, ChannelInterface::DISPLAY_NEWSLETTER_SUBSCRIBE_CHOICES)) { 28 | unset($displaySubscribeToNewsletterAtCheckout[$key]); 29 | } 30 | } 31 | $this->displaySubscribeToNewsletterAtCheckout = $displaySubscribeToNewsletterAtCheckout; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Model/CustomerInterface.php: -------------------------------------------------------------------------------- 1 | mailchimpState; 44 | } 45 | 46 | public function setMailchimpState(string $mailchimpState): void 47 | { 48 | $this->mailchimpState = $mailchimpState; 49 | } 50 | 51 | public function getMailchimpError(): ?string 52 | { 53 | return $this->mailchimpError; 54 | } 55 | 56 | public function setMailchimpError(?string $mailchimpError): void 57 | { 58 | $this->mailchimpError = $mailchimpError; 59 | } 60 | 61 | public function getMailchimpStateUpdatedAt(): ?DateTimeInterface 62 | { 63 | return $this->mailchimpStateUpdatedAt; 64 | } 65 | 66 | public function setMailchimpStateUpdatedAt(DateTimeInterface $mailchimpStateUpdatedAt): void 67 | { 68 | $this->mailchimpStateUpdatedAt = $mailchimpStateUpdatedAt; 69 | } 70 | 71 | public function getMailchimpTries(): int 72 | { 73 | return $this->mailchimpTries; 74 | } 75 | 76 | public function setMailchimpTries(int $mailchimpTries): void 77 | { 78 | $this->mailchimpTries = $mailchimpTries; 79 | } 80 | 81 | public function incrementMailchimpTries(): void 82 | { 83 | ++$this->mailchimpTries; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Model/OrderInterface.php: -------------------------------------------------------------------------------- 1 | audienceRepository = $audienceRepository; 26 | $this->channelContext = $channelContext; 27 | } 28 | 29 | public function getAudienceFromOrder(OrderInterface $order): ?AudienceInterface 30 | { 31 | $channel = $order->getChannel(); 32 | if (null === $channel) { 33 | return null; 34 | } 35 | 36 | return $this->audienceRepository->findOneByChannel($channel); 37 | } 38 | 39 | public function getAudienceFromCustomerOrders(CustomerInterface $customer): ?AudienceInterface 40 | { 41 | /** @var OrderInterface $order */ 42 | foreach ($customer->getOrders() as $order) { 43 | $audience = $this->getAudienceFromOrder($order); 44 | 45 | if (null !== $audience) { 46 | return $audience; 47 | } 48 | } 49 | 50 | return null; 51 | } 52 | 53 | public function getAudienceFromContext(): ?AudienceInterface 54 | { 55 | try { 56 | /** @var ChannelInterface $channel */ 57 | $channel = $this->channelContext->getChannel(); 58 | 59 | return $this->audienceRepository->findOneByChannel($channel); 60 | } catch (ChannelNotFoundException $exception) { 61 | return null; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Provider/AudienceProviderInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Resources/config/grids/setono_sylius_mailchimp_admin_audience.yaml: -------------------------------------------------------------------------------- 1 | sylius_grid: 2 | grids: 3 | setono_sylius_mailchimp_admin_audience: 4 | driver: 5 | options: 6 | class: "%setono_sylius_mailchimp.model.audience.class%" 7 | fields: 8 | audienceId: 9 | type: string 10 | label: setono_sylius_mailchimp.ui.audience_id 11 | name: 12 | type: string 13 | label: sylius.ui.name 14 | channel: 15 | type: string 16 | label: sylius.ui.channel 17 | actions: 18 | main: 19 | load: 20 | type: setono_sylius_mailchimp_load_audiences 21 | label: setono_sylius_mailchimp.ui.load_audiences 22 | options: 23 | link: 24 | route: setono_sylius_mailchimp_admin_load_audiences 25 | repush: 26 | type: setono_sylius_mailchimp_repush_customers 27 | label: setono_sylius_mailchimp.ui.repush_customers 28 | options: 29 | link: 30 | route: setono_sylius_mailchimp_admin_repush_customers 31 | item: 32 | update: 33 | type: update 34 | 35 | -------------------------------------------------------------------------------- /src/Resources/config/routing.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp_admin: 2 | resource: "@SetonoSyliusMailchimpPlugin/Resources/config/routing/admin.yaml" 3 | prefix: /admin 4 | 5 | setono_sylius_mailchimp_shop: 6 | resource: "@SetonoSyliusMailchimpPlugin/Resources/config/routing/shop.yaml" 7 | prefix: /{_locale} 8 | requirements: 9 | _locale: ^[a-z]{2}(?:_[A-Z]{2})?$ 10 | -------------------------------------------------------------------------------- /src/Resources/config/routing/admin.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp_admin_audience: 2 | resource: | 3 | section: admin 4 | alias: setono_sylius_mailchimp.audience 5 | only: ['index', 'update'] 6 | templates: '@SyliusAdmin\\Crud' 7 | permission: true 8 | redirect: update 9 | grid: setono_sylius_mailchimp_admin_audience 10 | vars: 11 | all: 12 | header: setono_sylius_mailchimp.ui.audience_header 13 | subheader: setono_sylius_mailchimp.ui.audience_subheader 14 | update: 15 | templates: 16 | form: "@SetonoSyliusMailchimpPlugin/Admin/Audience/_form.html.twig" 17 | type: sylius.resource 18 | 19 | setono_sylius_mailchimp_admin_load_audiences: 20 | path: /load-audiences 21 | methods: [GET] 22 | defaults: 23 | _controller: setono_sylius_mailchimp.controller.action.load_audiences 24 | 25 | setono_sylius_mailchimp_admin_repush_customers: 26 | path: /repush-customers 27 | methods: [GET] 28 | defaults: 29 | _controller: setono_sylius_mailchimp.controller.action.repush_customers 30 | -------------------------------------------------------------------------------- /src/Resources/config/routing/shop.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp_shop_ajax_subscribe_to_newsletter: 2 | path: /ajax/subscribe-to-newsletter 3 | methods: [POST] 4 | defaults: 5 | _controller: setono_sylius_mailchimp.controller.action.subscribe_to_newsletter 6 | 7 | setono_sylius_mailchimp_shop_partial_subscribe_to_newsletter: 8 | path: /_partial/subscribe-to-newsletter 9 | methods: [GET] 10 | defaults: 11 | _controller: setono_sylius_mailchimp.controller.action.subscribe_to_newsletter 12 | -------------------------------------------------------------------------------- /src/Resources/config/routing_non_localized.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp_admin: 2 | resource: "@SetonoSyliusMailchimpPlugin/Resources/config/routing/admin.yaml" 3 | prefix: /admin 4 | 5 | setono_sylius_mailchimp_shop: 6 | resource: "@SetonoSyliusMailchimpPlugin/Resources/config/routing/shop.yaml" 7 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Resources/config/services/block_event_listener.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | @SetonoSyliusMailchimpPlugin/Shop/_javascripts.html.twig 11 | 12 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Resources/config/services/client.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Resources/config/services/command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Resources/config/services/conditional/subscribe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | @SetonoSyliusMailchimpPlugin/Shop/subscribe.html.twig 11 | 12 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Resources/config/services/controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Resources/config/services/data_generator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/config/services/event_listener.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Resources/config/services/fixture.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Resources/config/services/form.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | setono_sylius_mailchimp 10 | 11 | 12 | setono_sylius_mailchimp 13 | 14 | 15 | 16 | 17 | 19 | %setono_sylius_mailchimp.model.audience.class% 20 | %setono_sylius_mailchimp.form.type.audience.validation_groups% 21 | 22 | 23 | 24 | 25 | 27 | %sylius.model.customer.class% 28 | %setono_sylius_mailchimp.form.type.customer_newsletter_subscription.validation_groups% 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/Resources/config/services/http_client.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 11 | %setono_sylius_mailchimp.api_key% 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Resources/config/services/loader.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Resources/config/services/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Resources/config/services/message.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/Resources/config/services/provider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Resources/public/js/shop/setono-mailchimp-subscribe.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | 4 | $.fn.extend({ 5 | subscribeToNewsletter: function () { 6 | let $form = $(this); 7 | $form.on('submit', function (event) { 8 | event.preventDefault(); 9 | 10 | let $status = $form.find('.setono-mailchimp-status'); 11 | $status.removeClass('negative').removeClass('positive').empty().hide(); 12 | 13 | $.ajax({ 14 | url: $form.attr('action'), 15 | type: $form.attr('method'), 16 | data: $form.serialize() 17 | }) 18 | .done(function (response) { 19 | $status.text(response.message).addClass('positive').show(); 20 | }) 21 | .fail(function (response) { 22 | $status.text(response.responseJSON.message).addClass('negative').show(); 23 | }); 24 | }); 25 | } 26 | }); 27 | })(jQuery); 28 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.da.yml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp: 2 | form: 3 | audience: 4 | notice: 5 | content: 6 | Når du forbinder en kanal med et publikum er det uopretteligt.
7 | Butik-id'et i Mailchimp er din kanalkode, og valutaen er din standardkanalvaluta.
8 | Du kan læse dokumentationen om Mailchimps e-handelsfunktioner here. 9 | header: Meddelelse 10 | channel: 11 | display_newsletter_subscribe: 12 | addressing: Addressering 13 | complete: Fuldført 14 | display_subscribe_to_newsletter_at_checkout: Vis tilmelding til nyhedsbrev ved checkouttrinet 15 | subscribe_to_newsletter: 16 | email: Email 17 | menu: 18 | admin: 19 | main: 20 | mailchimp: 21 | audiences: Publikum 22 | header: Mailchimp 23 | ui: 24 | an_error_occurred: Der er opstået en fejl 25 | api_key: Mailchimp API nøgle 26 | audience: Publikum 27 | audience_header: Mailchimp publikum 28 | audience_id: Audience Id 29 | audience_subheader: Administrer Mailchimp publikum 30 | audiences: Publikum 31 | channel_association_failed: Tilknyttelse af kanalen med en butik i Mailchimp fejlede. Fejlen(e) er logget blevet logget. 32 | code: Kode 33 | completed: Fuldført 34 | ecommerce: Mailchimp e-handelseksport 35 | email_not_blank: Email skal udfyldes. 36 | failed: Mislykkedes 37 | finished_at: Færdig klokken 38 | in_progress: I gang... 39 | invalid_csrf_token: Ugyldig CSRF token 40 | load_audiences: Indlæs publikum 41 | new: Ny 42 | newsletter: Nyhedsbrev 43 | no_audience_associated_with_channel: Der er ikke tilknyttet et publikum til denne kanal 44 | no_errors: Ingen fejl 45 | restarting: Genstarter... 46 | repush_customers: Genopfrisk kunder 47 | state: Stadie 48 | subscribe: Abonner 49 | subscribed_successfully: Du er nu tilmeldt vores nyhedsbrev. 50 | updated_at: Opdateret klokken 51 | your_email_address: Din emailadresse 52 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.en.yml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp: 2 | form: 3 | audience: 4 | notice: 5 | content: 6 | When you associate a channel with an audience it is irreversible.
7 | The store id in Mailchimp will be your channel code and the currency will be your default channel currency.
8 | You can read the documentation about Mailchimps ecommerce features here. 9 | header: NOTICE 10 | channel: 11 | display_newsletter_subscribe: 12 | addressing: Addressing 13 | complete: Complete 14 | display_subscribe_to_newsletter_at_checkout: Display newsletter subscribe at checkout steps 15 | subscribe_to_newsletter: 16 | email: Email 17 | menu: 18 | admin: 19 | main: 20 | mailchimp: 21 | audiences: Audiences 22 | header: Mailchimp 23 | ui: 24 | an_error_occurred: An error occurred 25 | api_key: Mailchimp API Key 26 | audience: Audience 27 | audience_header: Mailchimp audiences 28 | audience_id: Audience Id 29 | audience_subheader: Manage Mailchimp audiences 30 | audiences: Audiences 31 | channel_association_failed: Associating the channel with a store in Mailchimp failed. The error(s) has been logged. 32 | code: Code 33 | completed: Completed 34 | ecommerce: Mailchimp e-Commerce export 35 | email_not_blank: Email cannot be blank. 36 | failed: Failed 37 | finished_at: Finished at 38 | in_progress: In progress... 39 | invalid_csrf_token: Invalid CSRF token 40 | load_audiences: Load audiences 41 | new: New 42 | newsletter: Newsletter 43 | no_audience_associated_with_channel: No audience associated with this channel 44 | no_errors: No errors 45 | restarting: Restarting... 46 | repush_customers: Repush customers 47 | state: State 48 | subscribe: Subscribe 49 | subscribed_successfully: You are now subscribed to the newsletter. 50 | updated_at: Updated at 51 | your_email_address: Your email address 52 | -------------------------------------------------------------------------------- /src/Resources/translations/validators.da.yml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp: 2 | list: 3 | config: 4 | not_blank: Konfigurationen skal udfyldes. 5 | name: 6 | not_blank: Navn skal udfyldes. 7 | min_length: Navn skal minimum være {{ limit }} karakterer. 8 | max_length: Navn må ikke være længere end {{ limit }} karakterer. 9 | list_id: 10 | unique: Der er allerede en liste med dette ID. 11 | not_blank: Liste ID'et skal udfyldes. 12 | min_length: Liste ID'et skal være minimum {{ limit }} karakterer. 13 | max_length: Liste ID'et må ikke være længere end {{ limit }} karakterer. 14 | store_id: 15 | min_length: Navn skal minimum være {{ limit }} karakterer. 16 | max_length: Navn må ikke være længere end {{ limit }} karakterer. 17 | regex: Butiks-id kan kun bestå af bogstaver, tal, bindestreger og understreger. 18 | -------------------------------------------------------------------------------- /src/Resources/translations/validators.en.yml: -------------------------------------------------------------------------------- 1 | setono_sylius_mailchimp: 2 | list: 3 | config: 4 | not_blank: Config cannot be blank. 5 | name: 6 | not_blank: Name cannot be blank. 7 | min_length: Name must be at least {{ limit }} characters long. 8 | max_length: Name can not be longer than {{ limit }} characters. 9 | list_id: 10 | unique: There is an existing list with this ID. 11 | not_blank: List ID cannot be blank. 12 | min_length: List ID must be at least {{ limit }} characters long. 13 | max_length: List ID can not be longer than {{ limit }} characters. 14 | store_id: 15 | min_length: Name must be at least {{ limit }} characters long. 16 | max_length: Name can not be longer than {{ limit }} characters. 17 | regex: Store ID can only be comprised of letters, numbers, dashes and underscores. 18 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Audience/_form.html.twig: -------------------------------------------------------------------------------- 1 |
2 | {{ form_row(form.audienceId) }} 3 | 4 | {{ form_row(form.channel) }} 5 | 6 |
7 |
8 | {{ 'setono_sylius_mailchimp.form.audience.notice.header'|trans }} 9 |
10 |

{{ 'setono_sylius_mailchimp.form.audience.notice.content'|trans|raw }}

11 |
12 |
13 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Action/index.html.twig: -------------------------------------------------------------------------------- 1 | {% import '@SyliusUi/Macro/buttons.html.twig' as buttons %} 2 | 3 | {% set path = options.link.url|default(path(options.link.route, options.link.parameters|default({}))) %} 4 | {% set visible = options.visible is defined ? options.visible : true %} 5 | 6 | {% if visible %} 7 | {{ buttons.default(path, action.label, null, options.icon|default('list'), 'primary') }} 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Action/load_audiences.html.twig: -------------------------------------------------------------------------------- 1 | {% import '@SyliusUi/Macro/buttons.html.twig' as buttons %} 2 | 3 | {% set path = options.link.url|default(path(options.link.route)) %} 4 | 5 | {{ buttons.default(path, action.label, null, 'download', 'primary') }} 6 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Action/repush_customers.html.twig: -------------------------------------------------------------------------------- 1 | {% import '@SyliusUi/Macro/buttons.html.twig' as buttons %} 2 | 3 | {% set path = options.link.url|default(path(options.link.route)) %} 4 | 5 | {{ buttons.default(path, action.label, null, 'redo', 'gray') }} 6 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Field/array_count.html.twig: -------------------------------------------------------------------------------- 1 | {{ data|length }} 2 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Field/channel.html.twig: -------------------------------------------------------------------------------- 1 | {% import '@SyliusUi/Macro/labels.html.twig' as label %} 2 | 3 | {{ label.default(data.code) }} 4 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Field/collection.html.twig: -------------------------------------------------------------------------------- 1 | {% if options.vars.field_template is defined %} 2 | {% for item in data %} 3 | {% include options.vars.field_template with {'data': item} %} 4 | {% endfor %} 5 | {% else %} 6 | {{ data }} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Field/config.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ data.code }} 3 | 4 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Field/currency.html.twig: -------------------------------------------------------------------------------- 1 | {% import '@SyliusUi/Macro/labels.html.twig' as label %} 2 | 3 | {% if data is not null %} 4 | {{ label.default(data.code) }} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Field/list.html.twig: -------------------------------------------------------------------------------- 1 | {% import '@SyliusUi/Macro/labels.html.twig' as label %} 2 | 3 | 4 | {{ label.default(data.name) }} 5 | 6 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Grid/Field/state.html.twig: -------------------------------------------------------------------------------- 1 | {% set value = 'setono_sylius_mailchimp.ui.' ~ data %} 2 | 3 | {% if options.vars.labels is defined %} 4 | {% include [(options.vars.labels ~ '/' ~ data ~ '.html.twig'), '@SyliusUi/Label/_default.html.twig'] with {'value': value} %} 5 | {% else %} 6 | {% include '@SyliusUi/Label/_default.html.twig' with {'value': value} %} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /src/Resources/views/Admin/Macro/buttons.html.twig: -------------------------------------------------------------------------------- 1 | {% macro post(url, message, id, icon, class, requiresConfirmation = false) %} 2 |
3 | 4 | 15 |
16 | {% endmacro %} 17 | -------------------------------------------------------------------------------- /src/Resources/views/Shop/Subscribe/_form.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ form_row(form.email) }} 3 | 4 | -------------------------------------------------------------------------------- /src/Resources/views/Shop/Subscribe/content.html.twig: -------------------------------------------------------------------------------- 1 | {% form_theme form '@SyliusShop/Form/theme.html.twig' %} 2 | 3 | {{ form_start(form, {'action': path('setono_sylius_mailchimp_shop_ajax_subscribe_to_newsletter'), 'method': 'POST'}) }} 4 | {% include '@SetonoSyliusMailchimpPlugin/Shop/Subscribe/_form.html.twig' %} 5 | {{ form_end(form) }} 6 | -------------------------------------------------------------------------------- /src/Resources/views/Shop/_javascripts.html.twig: -------------------------------------------------------------------------------- 1 | {% include '@SyliusUi/_javascripts.html.twig' with {'path': 'bundles/setonosyliusmailchimpplugin/js/shop/setono-mailchimp-subscribe.js'} %} 2 | 5 | -------------------------------------------------------------------------------- /src/Resources/views/Shop/subscribe.html.twig: -------------------------------------------------------------------------------- 1 | {{ render(url('setono_sylius_mailchimp_shop_partial_subscribe_to_newsletter', {'template': template|default('@SetonoSyliusMailchimpPlugin/Shop/Subscribe/content.html.twig')})) }} 2 | -------------------------------------------------------------------------------- /src/SetonoSyliusMailchimpPlugin.php: -------------------------------------------------------------------------------- 1 |