├── 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 |
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 |
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 |
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 |
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 |