├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── mkdocs.yml ├── phpspec.yml ├── phpunit.xml.dist ├── spec ├── Controller │ └── WebhookControllerSpec.php ├── DependencyInjection │ └── ConfigurationSpec.php ├── Event │ ├── SubscriberEventSpec.php │ ├── SubscriberListenerSpec.php │ └── WebhookEventSpec.php └── Subscriber │ ├── ListRepositorySpec.php │ ├── ListSynchronizerSpec.php │ ├── SubscriberListSpec.php │ └── SubscriberSpec.php ├── src ├── Command │ ├── SynchronizeMergeFieldsCommand.php │ ├── SynchronizeSubscribersCommand.php │ └── WebhookCommand.php ├── Controller │ └── WebhookController.php ├── DependencyInjection │ ├── Configuration.php │ └── WelpMailchimpExtension.php ├── Event │ ├── SubscriberEvent.php │ ├── SubscriberListener.php │ └── WebhookEvent.php ├── Exception │ └── MailchimpException.php ├── Provider │ ├── ConfigListProvider.php │ ├── DynamicProviderInterface.php │ ├── ExampleSubscriberProvider.php │ ├── FosSubscriberProvider.php │ ├── ListProviderInterface.php │ ├── ProviderFactory.php │ └── ProviderInterface.php ├── Resources │ ├── config │ │ ├── routing.yml │ │ └── services.yml │ └── doc │ │ ├── CONTRIBUTING.md │ │ ├── configuration.md │ │ ├── documentation.md │ │ ├── index.md │ │ ├── list-provider.md │ │ ├── mailchimp-doc.md │ │ ├── setup.md │ │ ├── subscriber-language.md │ │ ├── subscriber-provider.md │ │ ├── tests.md │ │ ├── usage.md │ │ └── webhook.md ├── Subscriber │ ├── ListRepository.php │ ├── ListSynchronizer.php │ ├── Subscriber.php │ ├── SubscriberList.php │ └── SubscriberListInterface.php └── WelpMailchimpBundle.php └── tests ├── Command └── SynchronizeSubscribersCommandTest.php ├── Provider └── configListProviderTest.php └── Subscriber ├── ListRepositoryTest.php ├── ListSynchronizerTest.php └── SubscriberTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | vendor 3 | docs 4 | composer.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.6' 4 | - '7.1' 5 | 6 | before_script: 7 | - php -d memory_limit=-1 composer install --dev -v --prefer-source 8 | 9 | script: 10 | - bin/phpspec run -fpretty --verbose 11 | #- bin/phpunit -c . 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | * [Contributing to Open Source guide](https://guides.github.com/activities/contributing-to-open-source/) 4 | 5 | ## Steps 6 | 7 | 1. Create an Issue 8 | 2. Pull Request 9 | 3. Merge \o/ 10 | 11 | ## ToDo 12 | 13 | ☐ Moar unit tests!! 14 | ☐ Fix workaround to update User's email address (waiting for MailChimp API V3 update) 15 | ☐ Commands options for full sync, merge fields sync, webhook register (listId, ...) 16 | ☐ Spec documentation (http://phpdox.de/) 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Welpdev 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mailchimp-bundle 2 | 3 | [![Build Status](https://travis-ci.org/welpdev/mailchimp-bundle.svg?branch=master)](https://travis-ci.org/welpdev/mailchimp-bundle) 4 | [![Packagist](https://img.shields.io/packagist/v/welp/mailchimp-bundle.svg)](https://packagist.org/packages/welp/mailchimp-bundle) 5 | [![Packagist](https://img.shields.io/packagist/dt/welp/mailchimp-bundle.svg)](https://packagist.org/packages/welp/mailchimp-bundle) 6 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/welpdev/mailchimp-bundle/master/LICENSE.md) 7 | [![Documentation](https://img.shields.io/badge/documentation-gh--pages-blue.svg)](https://welpdev.github.io/mailchimp-bundle/) 8 | 9 | This bundle will help you synchronise your project's newsletter subscribers into MailChimp throught MailChimp API V3. 10 | 11 | ## Features 12 | 13 | * [x] Use your own userProvider (basic `FosSubscriberProvider` included to interface with FosUserBundle) 14 | * [x] Use your own listProvider (`DoctrineListProvider` included to retrieve your list from a database) 15 | * [x] Synchronize Merge Fields with your config 16 | * [x] Synchronize your subscriber with a List 17 | * [x] Use lifecycle event to subscribe/unsubscribe/delete subscriber from a List 18 | * [x] Retrieve [MailChimp Object](https://github.com/drewm/mailchimp-api) to make custom MailChimp API V3 requests 19 | * [x] Register Webhooks 20 | 21 | ## Setup 22 | 23 | Add bundle to your project: 24 | 25 | ```bash 26 | composer require welp/mailchimp-bundle 27 | ``` 28 | 29 | Add `Welp\MailchimpBundle\WelpMailchimpBundle` to your `AppKernel.php`: 30 | 31 | ```php 32 | $bundles = [ 33 | // ... 34 | new Welp\MailchimpBundle\WelpMailchimpBundle(), 35 | ]; 36 | ``` 37 | 38 | ## Minimal Configuration 39 | 40 | In your `config.yml`: 41 | 42 | ```yaml 43 | welp_mailchimp: 44 | api_key: YOURMAILCHIMPAPIKEY 45 | ``` 46 | 47 | More configuration on the [documentation](https://welpdev.github.io/mailchimp-bundle/configuration/). 48 | 49 | ## Full Documentation 50 | 51 | Look at the full documentation at 52 | 53 | * Setup 54 | * Configuration 55 | * Subscriber Provider 56 | * List Provider 57 | * Usage 58 | * Synchronize merge fields 59 | * Full synchronization with command 60 | * Unit synchronization with events 61 | * Subscribe new User 62 | * Unsubscribe a User 63 | * Update a User 64 | * Change User's email address (WORKAROUND) 65 | * Delete a User 66 | * Retrieve [MailChimp Object](https://github.com/drewm/mailchimp-api) to make custom MailChimp API V3 requests 67 | * Webhook 68 | * Update User when subscribe/unsubscribe 69 | 70 | ## Contributing 71 | 72 | If you want to contribute to this project, look at [over here](CONTRIBUTING.md) 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "welp/mailchimp-bundle", 3 | "description": "MailChimp API V3 Symfony Bundle", 4 | "type": "symfony-bundle", 5 | "homepage": "https://github.com/welpdev/mailchimp-bundle", 6 | "license": "MIT", 7 | "keywords": [ 8 | "Symfony", 9 | "MailChimp", 10 | "API", 11 | "V3", 12 | "User", 13 | "Subscriber" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Titouan BENOIT", 18 | "email": "titouan@welp.today" 19 | } 20 | ], 21 | "autoload": { 22 | "psr-4": { 23 | "Welp\\MailchimpBundle\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Tests\\": "tests/" 29 | } 30 | }, 31 | "require": { 32 | "php": ">=5.6", 33 | "drewm/mailchimp-api": "2.5.*" 34 | }, 35 | "require-dev": { 36 | "phpspec/phpspec": "~2@dev", 37 | "phpunit/phpunit": "~5", 38 | "symfony/symfony": ">=2.7" 39 | }, 40 | "config": { 41 | "bin-dir": "bin" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: MailChimp Bundle 2 | site_description: This bundle will help you synchronise your project's newsletter subscribers into MailChimp throught MailChimp API V3. 3 | site_author: Titouan BENOIT 4 | copyright: MIT 5 | repo_url: https://github.com/welpdev/mailchimp-bundle 6 | docs_dir: 'src/Resources/doc' 7 | site_dir: "docs" 8 | pages: 9 | - Home: index.md 10 | - Setup: setup.md 11 | - Configuration: configuration.md 12 | - Subscriber Provider: subscriber-provider.md 13 | - List Provider: list-provider.md 14 | - Usage: usage.md 15 | - Webhook: webhook.md 16 | - Tests: tests.md 17 | - Subscriber Languages: subscriber-language.md 18 | - MailChimp Doc: mailchimp-doc.md 19 | - Contributing: CONTRIBUTING.md 20 | theme: readthedocs 21 | markdown_extensions: 22 | - pymdownx.github 23 | -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | suites: 2 | default: 3 | namespace: Welp\MailchimpBundle 4 | psr4_prefix: Welp\MailchimpBundle -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | ./tests/ 9 | 10 | 11 | 12 | 13 | ./vendor 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/Controller/WebhookControllerSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Welp\MailchimpBundle\Controller\WebhookController'); 17 | $this->shouldHaveType('Symfony\Bundle\FrameworkBundle\Controller\Controller'); 18 | } 19 | 20 | function it_access_denied_if_secret_is_false() 21 | { 22 | $request = new Request(); 23 | $request->setMethod('POST'); 24 | 25 | $this->shouldThrow(new \Exception('incorrect data format!')) 26 | ->duringIndexAction($request); 27 | } 28 | 29 | /*function it_pass_when_secret_is_true(){ 30 | //$this->getParameter('welp_mailchimp.lists')->willReturn(['listid01' => ['webhook_secret' => 'mysecretTest']]); 31 | 32 | $request = new Request(); 33 | $request->setMethod('POST'); 34 | var_dump($request); die(); 35 | 36 | 37 | }*/ 38 | } 39 | -------------------------------------------------------------------------------- /spec/DependencyInjection/ConfigurationSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Welp\MailchimpBundle\DependencyInjection\Configuration'); 12 | } 13 | 14 | function it_is_symfony_configuration() 15 | { 16 | $this->shouldImplement('Symfony\Component\Config\Definition\ConfigurationInterface'); 17 | } 18 | 19 | function it_gets_config_tree_builder() 20 | { 21 | $this 22 | ->getConfigTreeBuilder() 23 | ->shouldHaveType('Symfony\Component\Config\Definition\Builder\TreeBuilder'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spec/Event/SubscriberEventSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('1337', $subscriber); 14 | } 15 | 16 | function it_is_initializable() 17 | { 18 | $this->shouldHaveType('Welp\MailchimpBundle\Event\SubscriberEvent'); 19 | $this->shouldHaveType('Symfony\Component\EventDispatcher\Event'); 20 | } 21 | 22 | function it_has_a_listname() 23 | { 24 | $this->getListId()->shouldReturn('1337'); 25 | } 26 | 27 | function it_has_a_subscriber($subscriber) 28 | { 29 | $this->getSubscriber()->shouldReturn($subscriber); 30 | } 31 | 32 | function it_has_old_email($subscriber){ 33 | $this->beConstructedWith('1337', $subscriber, 'oldemail@free.fr'); 34 | $this->getOldEmail('oldemail@free.fr'); 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spec/Event/SubscriberListenerSpec.php: -------------------------------------------------------------------------------- 1 | findById('foo')->willReturn(['id' => 123]); 16 | 17 | $event->getListId()->willReturn('foo'); 18 | $event->getSubscriber()->willReturn($subscriber); 19 | 20 | $this->beConstructedWith($listRepository); 21 | } 22 | 23 | function it_is_initializable() 24 | { 25 | $this->shouldHaveType('Welp\MailchimpBundle\Event\SubscriberListener'); 26 | } 27 | 28 | function it_listen_to_subscribe_events($listRepository, $event, $subscriber) 29 | { 30 | $listRepository->subscribe('foo', $subscriber)->shouldBeCalled(); 31 | $this->onSubscribe($event); 32 | } 33 | 34 | function it_listen_to_unsubscribe_events($listRepository, $event, $subscriber) 35 | { 36 | $listRepository->unsubscribe('foo', $subscriber)->shouldBeCalled(); 37 | $this->onUnsubscribe($event); 38 | } 39 | 40 | function it_listen_to_pending_events($listRepository, $event, $subscriber) 41 | { 42 | $listRepository->pending('foo', $subscriber)->shouldBeCalled(); 43 | $this->onPending($event); 44 | } 45 | 46 | function it_listen_to_clean_events($listRepository, $event, $subscriber) 47 | { 48 | $listRepository->clean('foo', $subscriber)->shouldBeCalled(); 49 | $this->onClean($event); 50 | } 51 | 52 | function it_listen_to_delete_events($listRepository, $event, $subscriber) 53 | { 54 | $listRepository->delete('foo', $subscriber)->shouldBeCalled(); 55 | $this->onDelete($event); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /spec/Event/WebhookEventSpec.php: -------------------------------------------------------------------------------- 1 | 0, 'data' => 154158]) 11 | { 12 | $this->beConstructedWith($data); 13 | } 14 | 15 | function it_is_initializable() 16 | { 17 | $this->shouldHaveType('Welp\MailchimpBundle\Event\WebhookEvent'); 18 | $this->shouldHaveType('Symfony\Component\EventDispatcher\Event'); 19 | } 20 | 21 | function it_has_data($data) 22 | { 23 | $this->getData()->shouldReturn($data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spec/Subscriber/ListRepositorySpec.php: -------------------------------------------------------------------------------- 1 | prepareSubscriber($subscriber); 16 | $this->prepareMailchimpLists($mailchimp); 17 | 18 | $this->beConstructedWith($mailchimp); 19 | } 20 | 21 | public function it_is_initializable() 22 | { 23 | $this->shouldHaveType('Welp\MailchimpBundle\Subscriber\ListRepository'); 24 | } 25 | 26 | public function it_can_get_the_mailchimp_object() 27 | { 28 | $this->getMailChimp()->shouldHaveType('\DrewM\MailChimp\MailChimp'); 29 | } 30 | 31 | public function it_can_find_a_list_by_its_id(MailChimp $mailchimp) 32 | { 33 | $this->findById('ba039c6198')->shouldReturn(['id' => 'ba039c6198', 'name' => 'myList']); 34 | } 35 | 36 | public function it_can_not_find_a_list_by_its_id(MailChimp $mailchimp) 37 | { 38 | $mailchimp->success()->willReturn(false); 39 | $mailchimp->getLastResponse()->willReturn([ 40 | "headers" => [], 41 | "body" => '{"type":"http://developer.mailchimp.com/documentation/mailchimp/guides/error-glossary/","title":"Invalid Resource","status":404,"detail":"The requested resource could not be found.","instance":""}' 42 | ]); 43 | 44 | $this->shouldThrow(new MailchimpException(404, 'The requested resource could not be found.', "http://developer.mailchimp.com/documentation/mailchimp/guides/error-glossary/", "Invalid Resource", null, '')) 45 | ->duringFindById('notfound'); 46 | } 47 | 48 | public function it_subscribe_a_subscriber(MailChimp $mailchimp, $subscriber) 49 | { 50 | $this->subscribe('ba039c6198', $subscriber)->shouldReturn([ 51 | 'email_address' => 'charles@terrasse.fr', 52 | 'merge_fields' => ['FNAME' => 'Charles', 'LNAME' => 'Terrasse'], 53 | 'language' => 'fr', 54 | 'email_type' => 'html', 55 | 'status' => 'subscribed' 56 | ]); 57 | } 58 | 59 | public function it_unsubscribe_a_subscriber(MailChimp $mailchimp, $subscriber) 60 | { 61 | $this->unsubscribe('ba039c6198', $subscriber)->shouldReturn('unsubscribed'); 62 | } 63 | 64 | public function it_pending_a_subscriber(MailChimp $mailchimp, $subscriber) 65 | { 66 | $this->pending('ba039c6198', $subscriber)->shouldReturn('pending'); 67 | } 68 | 69 | public function it_clean_a_subscriber(MailChimp $mailchimp, $subscriber) 70 | { 71 | $this->clean('ba039c6198', $subscriber)->shouldReturn('cleaned'); 72 | } 73 | 74 | public function it_delete_a_subscriber(MailChimp $mailchimp, $subscriber) 75 | { 76 | $mailchimp->delete("lists/ba039c6198/members/b1a29fd58778c40c7f15f06a334dc691")->willReturn('deleted'); 77 | 78 | $this->delete('ba039c6198', $subscriber)->shouldReturn('deleted'); 79 | } 80 | 81 | public function it_update_a_subscriber(MailChimp $mailchimp, $subscriber) 82 | { 83 | $mailchimp->patch("lists/ba039c6198/members/b1a29fd58778c40c7f15f06a334dc691", ["email_address" => "charles@terrasse.fr", "merge_fields" => ["FNAME" => "Charles", "LNAME" => "Terrasse"], "language" => "fr", "email_type" => "html"])->willReturn('update'); 84 | 85 | $this->update('ba039c6198', $subscriber)->shouldReturn('update'); 86 | } 87 | 88 | public function it_finds_merge_tags(MailChimp $mailchimp) 89 | { 90 | $mailchimp 91 | ->get("lists/123/merge-fields") 92 | ->willReturn( 93 | [ 94 | 'merge_fields' => [ 95 | ['tag' => 'EMAIL', 'name' => 'email'], 96 | ['tag' => 'FOO', 'name' => 'Bar'], 97 | ], 98 | 'total_items' => 2 99 | ]); 100 | 101 | $this->getMergeFields(123)->shouldReturn([['tag' => 'EMAIL', 'name' => 'email'], ['tag' => 'FOO', 'name' => 'Bar']]); 102 | } 103 | 104 | public function it_finds_merge_tags_more_than_10(MailChimp $mailchimp) 105 | { 106 | $mailchimp 107 | ->get("lists/123/merge-fields") 108 | ->willReturn( 109 | [ 110 | 'merge_fields' => [ 111 | ['tag' => 'FOO1', 'name' => 'Bar'], 112 | ['tag' => 'FOO2', 'name' => 'Bar'], 113 | ['tag' => 'FOO3', 'name' => 'Bar'], 114 | ['tag' => 'FOO4', 'name' => 'Bar'], 115 | ['tag' => 'FOO5', 'name' => 'Bar'], 116 | ['tag' => 'FOO6', 'name' => 'Bar'], 117 | ['tag' => 'FOO8', 'name' => 'Bar'], 118 | ['tag' => 'FOO9', 'name' => 'Bar'], 119 | ['tag' => 'FOO10', 'name' => 'Bar'], 120 | ], 121 | 'total_items' => 13 122 | ]); 123 | 124 | $mailchimp 125 | ->get("lists/123/merge-fields", array("count" => 13)) 126 | ->willReturn( 127 | [ 128 | 'merge_fields' => [ 129 | ['tag' => 'FOO1', 'name' => 'Bar'], 130 | ['tag' => 'FOO2', 'name' => 'Bar'], 131 | ['tag' => 'FOO3', 'name' => 'Bar'], 132 | ['tag' => 'FOO4', 'name' => 'Bar'], 133 | ['tag' => 'FOO5', 'name' => 'Bar'], 134 | ['tag' => 'FOO6', 'name' => 'Bar'], 135 | ['tag' => 'FOO8', 'name' => 'Bar'], 136 | ['tag' => 'FOO9', 'name' => 'Bar'], 137 | ['tag' => 'FOO10', 'name' => 'Bar'], 138 | ['tag' => 'FOO12', 'name' => 'Bar'], 139 | ['tag' => 'FOO13', 'name' => 'Bar'], 140 | ], 141 | 'total_items' => 13 142 | ]); 143 | 144 | $this->getMergeFields(123)->shouldReturn([ 145 | ['tag' => 'FOO1', 'name' => 'Bar'], 146 | ['tag' => 'FOO2', 'name' => 'Bar'], 147 | ['tag' => 'FOO3', 'name' => 'Bar'], 148 | ['tag' => 'FOO4', 'name' => 'Bar'], 149 | ['tag' => 'FOO5', 'name' => 'Bar'], 150 | ['tag' => 'FOO6', 'name' => 'Bar'], 151 | ['tag' => 'FOO8', 'name' => 'Bar'], 152 | ['tag' => 'FOO9', 'name' => 'Bar'], 153 | ['tag' => 'FOO10', 'name' => 'Bar'], 154 | ['tag' => 'FOO12', 'name' => 'Bar'], 155 | ['tag' => 'FOO13', 'name' => 'Bar'], 156 | ]); 157 | } 158 | 159 | public function it_deletes_a_merge_tag(MailChimp $mailchimp) 160 | { 161 | $mailchimp->delete("lists/123/merge-fields/foo")->shouldBeCalled(); 162 | 163 | $this->deleteMergeField(123, 'foo'); 164 | } 165 | 166 | public function it_adds_a_merge_tag(MailChimp $mailchimp) 167 | { 168 | $mailchimp->post("lists/123/merge-fields", $mergeData = [ 169 | 'tag' => 'FOO', 170 | 'name' => 'Foo bar', 171 | 'options' => ['req' => true] 172 | ])->shouldBeCalled(); 173 | 174 | $this->addMergeField(123, $mergeData); 175 | } 176 | 177 | public function it_updates_a_merge_tag(MailChimp $mailchimp) 178 | { 179 | $mailchimp->patch("lists/123/merge-fields/2", $mergeData = [ 180 | 'tag' => 'FOO', 181 | 'name' => 'Foo bar', 182 | 'options' => ['req' => true] 183 | ])->shouldBeCalled(); 184 | 185 | $this->updateMergeField(123, 2, $mergeData); 186 | } 187 | 188 | protected function prepareSubscriber(Subscriber $subscriber) 189 | { 190 | $subscriber->getEmail()->willReturn('charles@terrasse.fr'); 191 | $subscriber->getMergeFields()->willReturn(['FNAME' => 'Charles', 'LNAME' => 'Terrasse']); 192 | $subscriber->getOptions()->willReturn(['language' => 'fr', 'email_type' => 'html']); 193 | $subscriber->formatMailChimp()->willReturn(['email_address' => 'charles@terrasse.fr', 'merge_fields' => ['FNAME' => 'Charles', 'LNAME' => 'Terrasse'], 'language' => 'fr', 'email_type' => 'html']); 194 | } 195 | 196 | protected function prepareMailchimpLists(MailChimp $mailchimp) 197 | { 198 | // success 199 | $mailchimp->success()->willReturn(true); 200 | // get the list 201 | $mailchimp->get("lists/ba039c6198")->willReturn(['id' => 'ba039c6198', 'name' => 'myList']); 202 | $mailchimp->get("lists/notfound")->willReturn(null); 203 | // subscribe member 204 | $mailchimp->put("lists/ba039c6198/members/b1a29fd58778c40c7f15f06a334dc691", [ 205 | 'email_address' => 'charles@terrasse.fr', 206 | 'merge_fields' => ['FNAME' => 'Charles', 'LNAME' => 'Terrasse'], 207 | 'language' => 'fr', 208 | 'email_type' => 'html', 209 | 'status' => 'subscribed' 210 | ])->willReturn([ 211 | 'email_address' => 'charles@terrasse.fr', 212 | 'merge_fields' => ['FNAME' => 'Charles', 'LNAME' => 'Terrasse'], 213 | 'language' => 'fr', 214 | 'email_type' => 'html', 215 | 'status' => 'subscribed' 216 | ]); 217 | 218 | $mailchimp->put("lists/ba039c6198/members/b1a29fd58778c40c7f15f06a334dc691", [ 219 | 'email_address' => 'charles@terrasse.fr', 220 | 'merge_fields' => ['FNAME' => 'Charles', 'LNAME' => 'Terrasse'], 221 | 'language' => 'fr', 222 | 'email_type' => 'html', 223 | 'status' => 'unsubscribed' 224 | ])->willReturn('unsubscribed'); 225 | 226 | $mailchimp->put("lists/ba039c6198/members/b1a29fd58778c40c7f15f06a334dc691", [ 227 | 'email_address' => 'charles@terrasse.fr', 228 | 'merge_fields' => ['FNAME' => 'Charles', 'LNAME' => 'Terrasse'], 229 | 'language' => 'fr', 230 | 'email_type' => 'html', 231 | 'status' => 'pending' 232 | ])->willReturn('pending'); 233 | 234 | $mailchimp->put("lists/ba039c6198/members/b1a29fd58778c40c7f15f06a334dc691", [ 235 | 'email_address' => 'charles@terrasse.fr', 236 | 'merge_fields' => ['FNAME' => 'Charles', 'LNAME' => 'Terrasse'], 237 | 'language' => 'fr', 238 | 'email_type' => 'html', 239 | 'status' => 'cleaned' 240 | ])->willReturn('cleaned'); 241 | } 242 | 243 | protected function getSubscriberChunk($count, $offset) 244 | { 245 | $subscribers = []; 246 | for ($i = $offset; $i < $offset + $count; $i++) { 247 | $subscribers[] = new Subscriber(sprintf('email%s@example.org', $i)); 248 | } 249 | 250 | return $subscribers; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /spec/Subscriber/ListSynchronizerSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($listRepository); 15 | } 16 | 17 | function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Welp\MailchimpBundle\Subscriber\ListSynchronizer'); 20 | } 21 | 22 | function it_synchronize_merge_tags($listRepository, SubscriberList $list) 23 | { 24 | $listRepository->findById('foobar')->willReturn(['id' => 'foobar']); 25 | $listRepository->getMergeFields('foobar')->willReturn([ 26 | ['merge_id' => 1, 'tag' => 'TAG1', 'name' => 'Tag 1'], 27 | ['merge_id' => 2, 'tag' => 'OBSOLETE', 'name' => 'This tag should not exist'], 28 | ]); 29 | 30 | $listRepository->deleteMergeField('foobar', 2)->shouldBeCalled(); 31 | $listRepository->updateMergeField('foobar', 1, ['tag' => 'TAG1', 'name' => 'Tag 1', 'options' => ['req' => true]])->shouldBeCalled(); 32 | $listRepository->addMergeField('foobar', ['tag' => 'TAG2', 'name' => 'Tag 2', 'options' => ['req' => false]])->shouldBeCalled(); 33 | 34 | $this->synchronizeMergeFields('foobar', [ 35 | ['tag' => 'TAG1', 'name' => 'Tag 1', 'options' => ['req' => true]], 36 | ['tag' => 'TAG2', 'name' => 'Tag 2', 'options' => ['req' => false]], 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/Subscriber/SubscriberListSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('1337', $provider); 14 | } 15 | 16 | function it_is_initializable() 17 | { 18 | $this->shouldHaveType('Welp\MailchimpBundle\Subscriber\SubscriberList'); 19 | } 20 | 21 | function it_has_a_name() 22 | { 23 | $this->getListId()->shouldReturn('1337'); 24 | } 25 | 26 | function it_has_a_provider($provider) 27 | { 28 | $this->getProvider()->shouldReturn($provider); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spec/Subscriber/SubscriberSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('charles@terrasse.fr'); 13 | } 14 | 15 | public function it_is_initializable() 16 | { 17 | $this->shouldHaveType('Welp\MailchimpBundle\Subscriber\Subscriber'); 18 | } 19 | 20 | public function it_has_an_email() 21 | { 22 | $this->getEmail()->shouldReturn('charles@terrasse.fr'); 23 | } 24 | 25 | public function it_has_default_merge_fields() 26 | { 27 | $this->getMergeFields()->shouldReturn([]); 28 | } 29 | 30 | public function it_can_have_merge_fields() 31 | { 32 | $this->beConstructedWith('charles@terrasse.fr', $tags = ['FIRSTNAME' => 'Charles']); 33 | $this->getMergeFields()->shouldReturn($tags); 34 | } 35 | 36 | public function it_can_have_merge_fields_with_null_value() 37 | { 38 | $this->beConstructedWith('charles@terrasse.fr', $tags = ['FIRSTNAME' => 'Charles', 'BIRTHDATE' => null]); 39 | $this->getMergeFields()->shouldReturn(['FIRSTNAME' => 'Charles']); 40 | } 41 | 42 | public function it_can_have_options() 43 | { 44 | $this->beConstructedWith('charles@terrasse.fr', $tags = ['FIRSTNAME' => 'Charles'], $options = ['language' => "fr"]); 45 | $this->getOptions()->shouldReturn($options); 46 | } 47 | 48 | public function it_format_for_mailchimp() 49 | { 50 | $this->beConstructedWith('charles@terrasse.fr', $tags = ['FIRSTNAME' => 'Charles'], $options = ['language' => "fr"]); 51 | $this->formatMailChimp()->shouldReturn([ 52 | 'email_address' => 'charles@terrasse.fr', 53 | 'merge_fields' => $tags, 54 | 'language' => "fr" 55 | ]); 56 | } 57 | 58 | public function it_format_for_mailchimp_without_mergetags() 59 | { 60 | $this->beConstructedWith('charles@terrasse.fr', [], $options = ['language' => "fr"]); 61 | $this->formatMailChimp()->shouldReturn([ 62 | 'email_address' => 'charles@terrasse.fr', 63 | 'language' => "fr" 64 | ]); 65 | } 66 | 67 | public function it_format_for_mailchimp_without_options() 68 | { 69 | $this->beConstructedWith('charles@terrasse.fr', [], []); 70 | $this->formatMailChimp()->shouldReturn([ 71 | 'email_address' => 'charles@terrasse.fr' 72 | ]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Command/SynchronizeMergeFieldsCommand.php: -------------------------------------------------------------------------------- 1 | listSynchronizer = $listSynchronizer; 30 | $this->listProvider = $listProvider; 31 | 32 | parent::__construct(); 33 | } 34 | 35 | protected function configure() 36 | { 37 | $this 38 | ->setDescription('Synchronizing merge fields in MailChimp') 39 | ->setName('welp:mailchimp:synchronize-merge-fields') 40 | // @TODO add params : listId 41 | ; 42 | } 43 | 44 | protected function execute(InputInterface $input, OutputInterface $output) 45 | { 46 | $output->writeln(sprintf('%s', $this->getDescription())); 47 | 48 | $lists = $this->listProvider->getLists(); 49 | 50 | foreach ($lists as $list) { 51 | $this->listSynchronizer->synchronizeMergeFields($list->getListId(), $list->getMergeFields()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/SynchronizeSubscribersCommand.php: -------------------------------------------------------------------------------- 1 | listSynchronizer = $listSynchronizer; 39 | $this->listProvider = $listProvider; 40 | $this->mailchimp = $mailchimp; 41 | 42 | parent::__construct(); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | protected function configure() 49 | { 50 | $this 51 | ->setDescription('Synchronizing subscribers in MailChimp') 52 | ->setName('welp:mailchimp:synchronize-subscribers') 53 | ->addOption( 54 | 'follow-sync', 55 | null, 56 | InputOption::VALUE_NONE, 57 | 'If you want to follow batches execution' 58 | ) 59 | // @TODO add params : listId, providerServiceKey 60 | ; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | protected function execute(InputInterface $input, OutputInterface $output) 67 | { 68 | $output->writeln(sprintf('%s', $this->getDescription())); 69 | 70 | $lists = $this->listProvider->getLists(); 71 | 72 | foreach ($lists as $list) { 73 | $output->writeln(sprintf('Synchronize list %s', $list->getListId())); 74 | $batchesResult = $this->listSynchronizer->synchronize($list); 75 | if ($input->getOption('follow-sync')) { 76 | while (!$this->batchesFinished($batchesResult)) { 77 | $batchesResult = $this->refreshBatchesResult($batchesResult); 78 | foreach ($batchesResult as $key => $batch) { 79 | $output->writeln($this->displayBatchInfo($batch)); 80 | } 81 | sleep(2); 82 | } 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Refresh all batch from MailChimp API. 89 | * 90 | * @param array $batchesResult 91 | * 92 | * @return array 93 | */ 94 | private function refreshBatchesResult($batchesResult) 95 | { 96 | $refreshedBatchsResults = []; 97 | 98 | foreach ($batchesResult as $key => $batch) { 99 | $batch = $this->mailchimp->get('batches/'.$batch['id']); 100 | array_push($refreshedBatchsResults, $batch); 101 | } 102 | 103 | return $refreshedBatchsResults; 104 | } 105 | 106 | /** 107 | * Test if all batches are finished. 108 | * 109 | * @param array $batchesResult 110 | * 111 | * @return bool 112 | */ 113 | private function batchesFinished($batchesResult) 114 | { 115 | $allfinished = true; 116 | foreach ($batchesResult as $key => $batch) { 117 | if ('finished' != $batch['status']) { 118 | $allfinished = false; 119 | } 120 | } 121 | 122 | return $allfinished; 123 | } 124 | 125 | /** 126 | * Pretty display of batch info. 127 | * 128 | * @param array $batch 129 | * 130 | * @return string 131 | */ 132 | private function displayBatchInfo($batch) 133 | { 134 | if ('finished' == $batch['status']) { 135 | return sprintf('batch %s is finished, operations %d/%d with %d errors. http responses: %s', $batch['id'], $batch['finished_operations'], $batch['total_operations'], $batch['errored_operations'], $batch['response_body_url']); 136 | } 137 | 138 | return sprintf('batch %s, current status %s, operations %d/%d with %d errors', $batch['id'], $batch['status'], $batch['finished_operations'], $batch['total_operations'], $batch['errored_operations']); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Command/WebhookCommand.php: -------------------------------------------------------------------------------- 1 | listProvider = $listProvider; 30 | $this->listRepository = $listRepository; 31 | 32 | parent::__construct(); 33 | } 34 | 35 | protected function configure() 36 | { 37 | $this 38 | ->setDescription('Add main webhook to a MailChimp List') 39 | ->setName('welp:mailchimp:webhook') 40 | ; 41 | // @TODO add params : listId, webhookurl 42 | } 43 | 44 | protected function execute(InputInterface $input, OutputInterface $output) 45 | { 46 | $output->writeln(sprintf('%s', $this->getDescription())); 47 | 48 | $lists = $this->listProvider->getLists(); 49 | 50 | foreach ($lists as $list) { 51 | $url = $list->getWebhookUrl().'?hooksecret='.$list->getWebhookSecret(); 52 | $output->writeln('Add webhook to list: '.$list->getListId()); 53 | $output->writeln('Webhook url: '.$url); 54 | 55 | $this->listRepository->registerMainWebhook($list->getListId(), $url); 56 | } 57 | 58 | $output->writeln('✔ done'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Controller/WebhookController.php: -------------------------------------------------------------------------------- 1 | isMethod('GET')) { 35 | return new JsonResponse([ 36 | 'success' => true, 37 | 'ping' => 'pong', 38 | ]); 39 | } 40 | 41 | // Handle POST request of Mailchimp 42 | $type = $request->request->get('type'); 43 | $data = $request->request->get('data'); 44 | /* Response example: 45 | data[merges][FNAME]: Tztz 46 | data[merges][EMAIL]: tztz@gmail.com 47 | data[email_type]: html 48 | data[reason]: manual 49 | data[email]: tztz@gmail.com 50 | data[id]: 5c1b5a7c1e 51 | data[merges][LNAME]: TZST 52 | data[list_id]: ba039c6198 53 | data[web_id]: 3375995 54 | */ 55 | $hooksecret = $request->query->get('hooksecret'); 56 | 57 | if (empty($type) || empty($data) || empty($hooksecret) || !array_key_exists('list_id', $data)) { 58 | throw new AccessDeniedHttpException('incorrect data format!'); 59 | } 60 | 61 | $listId = $data['list_id']; 62 | 63 | $listProviderKey = $this->getParameter('welp_mailchimp.list_provider'); 64 | try { 65 | $listProvider = $this->get($listProviderKey); 66 | } catch (ServiceNotFoundException $e) { 67 | throw new \InvalidArgumentException(sprintf('List Provider "%s" should be defined as a service.', $listProviderKey), $e->getCode(), $e); 68 | } 69 | 70 | if (!$listProvider instanceof ListProviderInterface) { 71 | throw new \InvalidArgumentException(sprintf('List Provider "%s" should implement Welp\MailchimpBundle\Provider\ListProviderInterface.', $listProviderKey)); 72 | } 73 | 74 | $list = $listProvider->getList($listId); 75 | 76 | // Check the webhook_secret 77 | $authorized = false; 78 | if($list != null && $list->getWebhookSecret() == $hooksecret) { 79 | $authorized = true; 80 | } 81 | 82 | if (!$authorized) { 83 | throw new AccessDeniedHttpException('Webhook secret mismatch!'); 84 | } 85 | 86 | // Trigger the right event 87 | switch ($type) { 88 | case 'subscribe': 89 | $dispatcher = $this->get('event_dispatcher'); 90 | $dispatcher->dispatch(WebhookEvent::EVENT_SUBSCRIBE, new WebhookEvent($data)); 91 | break; 92 | case 'unsubscribe': 93 | $dispatcher = $this->get('event_dispatcher'); 94 | $dispatcher->dispatch(WebhookEvent::EVENT_UNSUBSCRIBE, new WebhookEvent($data)); 95 | break; 96 | case 'profile': 97 | $dispatcher = $this->get('event_dispatcher'); 98 | $dispatcher->dispatch(WebhookEvent::EVENT_PROFILE, new WebhookEvent($data)); 99 | break; 100 | case 'cleaned': 101 | $dispatcher = $this->get('event_dispatcher'); 102 | $dispatcher->dispatch(WebhookEvent::EVENT_CLEANED, new WebhookEvent($data)); 103 | break; 104 | case 'upemail': 105 | $dispatcher = $this->get('event_dispatcher'); 106 | $dispatcher->dispatch(WebhookEvent::EVENT_UPEMAIL, new WebhookEvent($data)); 107 | break; 108 | case 'campaign': 109 | $dispatcher = $this->get('event_dispatcher'); 110 | $dispatcher->dispatch(WebhookEvent::EVENT_CAMPAIGN, new WebhookEvent($data)); 111 | break; 112 | default: 113 | throw new AccessDeniedHttpException('type mismatch!'); 114 | break; 115 | } 116 | 117 | return new JsonResponse([ 118 | 'type' => $type, 119 | 'data' => $data, 120 | ]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 24 | } else { 25 | $rootNode = $treeBuilder->root('welp_mailchimp'); 26 | } 27 | 28 | $rootNode 29 | ->children() 30 | ->scalarNode('api_key') 31 | ->isRequired() 32 | ->end() 33 | ->scalarNode('list_provider') 34 | ->defaultValue('welp_mailchimp.list_provider') 35 | ->end() 36 | // lists 37 | ->arrayNode('lists') 38 | ->useAttributeAsKey('listId') 39 | ->prototype('array') 40 | ->children() 41 | ->scalarNode('subscriber_provider')->end() 42 | ->scalarNode('webhook_secret')->end() 43 | ->scalarNode('webhook_url')->end() 44 | // merge_fields 45 | ->arrayNode('merge_fields') 46 | ->prototype('array') 47 | ->children() 48 | ->scalarNode('tag') 49 | ->isRequired() 50 | ->end() 51 | ->scalarNode('name') 52 | ->isRequired() 53 | ->end() 54 | ->scalarNode('type') 55 | ->isRequired() 56 | ->end() 57 | ->booleanNode('required') 58 | ->end() 59 | ->scalarNode('default_value') 60 | ->end() 61 | ->booleanNode('public') 62 | ->end() 63 | ->integerNode('display_order') 64 | ->end() 65 | // tag options 66 | ->arrayNode('options') 67 | ->children() 68 | ->integerNode('default_country') 69 | ->end() 70 | ->scalarNode('phone_format') 71 | ->end() 72 | ->scalarNode('date_format') 73 | ->end() 74 | ->arrayNode('choices') 75 | ->prototype('scalar')->end() 76 | ->end() 77 | ->integerNode('size') 78 | ->end() 79 | ->end() 80 | ->end() 81 | ->scalarNode('help_text') 82 | ->end() 83 | ->end() 84 | ->end() 85 | ->end() 86 | ->end() 87 | ->end() 88 | ->end() 89 | ->end() 90 | ; 91 | 92 | return $treeBuilder; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/DependencyInjection/WelpMailchimpExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 17 | $container->setParameter('welp_mailchimp.lists', $config['lists']); 18 | 19 | $container->setParameter('welp_mailchimp.list_provider', $config['list_provider']); 20 | $container->setParameter('welp_mailchimp.api_key', isset($config['api_key']) ? $config['api_key'] : null); 21 | 22 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 23 | $loader->load('services.yml'); 24 | 25 | // create an alias for the chosen list provider service 26 | $alias = $config['list_provider']; 27 | $container->setAlias('welp_mailchimp.list_provider.current', $alias); 28 | 29 | // Load all the used subscriber providers in the factory 30 | $this->loadSubscriberProviders($container, $config['lists']); 31 | } 32 | 33 | public function getAlias(): string 34 | { 35 | return 'welp_mailchimp'; 36 | } 37 | 38 | public function loadSubscriberProviders(ContainerBuilder $container, $lists) 39 | { 40 | $providerFactory = $container->getDefinition('welp_mailchimp.provider.factory'); 41 | foreach ($lists as $list) { 42 | $providerKey = $list['subscriber_provider']; 43 | $providerFactory->addMethodCall('addProvider', [$providerKey, new Reference($providerKey)]); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Event/SubscriberEvent.php: -------------------------------------------------------------------------------- 1 | listId = $listId; 82 | $this->subscriber = $subscriber; 83 | $this->oldEmail = $oldEmail; 84 | } 85 | 86 | /** 87 | * Get MailChimp listId 88 | * @return string 89 | */ 90 | public function getListId() 91 | { 92 | return $this->listId; 93 | } 94 | 95 | /** 96 | * Get the User as Subscriber 97 | * @return Subscriber 98 | */ 99 | public function getSubscriber() 100 | { 101 | return $this->subscriber; 102 | } 103 | 104 | /** 105 | * Get Subscriber's oldEmail (used for EVENT_CHANGE_EMAIL) 106 | * @return string 107 | */ 108 | public function getOldEmail() 109 | { 110 | return $this->oldEmail; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Event/SubscriberListener.php: -------------------------------------------------------------------------------- 1 | listRepository = $listRepository; 24 | } 25 | 26 | /** 27 | * Action when a User subscribe 28 | * @param SubscriberEvent $event 29 | * @return void 30 | */ 31 | public function onSubscribe(SubscriberEvent $event) 32 | { 33 | $this->listRepository->subscribe( 34 | $event->getListId(), 35 | $event->getSubscriber() 36 | ); 37 | } 38 | 39 | /** 40 | * Action when a User unsubscribe 41 | * @param SubscriberEvent $event 42 | * @return void 43 | */ 44 | public function onUnsubscribe(SubscriberEvent $event) 45 | { 46 | $this->listRepository->unsubscribe( 47 | $event->getListId(), 48 | $event->getSubscriber() 49 | ); 50 | } 51 | 52 | /** 53 | * Action when a User is pending 54 | * @param SubscriberEvent $event 55 | * @return void 56 | */ 57 | public function onPending(SubscriberEvent $event) 58 | { 59 | $this->listRepository->pending( 60 | $event->getListId(), 61 | $event->getSubscriber() 62 | ); 63 | } 64 | 65 | /** 66 | * Action when a User is cleaned 67 | * @param SubscriberEvent $event 68 | * @return void 69 | */ 70 | public function onClean(SubscriberEvent $event) 71 | { 72 | $this->listRepository->clean( 73 | $event->getListId(), 74 | $event->getSubscriber() 75 | ); 76 | } 77 | 78 | /** 79 | * Action when a User update its info 80 | * @param SubscriberEvent $event 81 | * @return void 82 | */ 83 | public function onUpdate(SubscriberEvent $event) 84 | { 85 | $this->listRepository->update( 86 | $event->getListId(), 87 | $event->getSubscriber() 88 | ); 89 | } 90 | 91 | /** 92 | * Action when a User change its email address 93 | * @param SubscriberEvent $event 94 | * @return void 95 | */ 96 | public function onChangeEmail(SubscriberEvent $event) 97 | { 98 | $this->listRepository->changeEmailAddress( 99 | $event->getListId(), 100 | $event->getSubscriber(), 101 | $event->getOldEmail() 102 | ); 103 | } 104 | 105 | /** 106 | * Action when a User is deleted 107 | * @param SubscriberEvent $event 108 | * @return void 109 | */ 110 | public function onDelete(SubscriberEvent $event) 111 | { 112 | $this->listRepository->delete( 113 | $event->getListId(), 114 | $event->getSubscriber() 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Event/WebhookEvent.php: -------------------------------------------------------------------------------- 1 | data = $data; 56 | } 57 | 58 | /** 59 | * Get data form webhook request 60 | * @return array 61 | */ 62 | public function getData() 63 | { 64 | return $this->data; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Exception/MailchimpException.php: -------------------------------------------------------------------------------- 1 | status = $status; 66 | $this->detail = $detail; 67 | $this->type = $type; 68 | $this->title = $title; 69 | $this->errors = $errors; 70 | $this->instance = $instance; 71 | } 72 | 73 | /** 74 | * @param string $type 75 | * 76 | * @return static 77 | */ 78 | public function setType($type) 79 | { 80 | $this->type = $type; 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param string $title 86 | * 87 | * @return static 88 | */ 89 | public function setTitle($title) 90 | { 91 | $this->title = $title; 92 | return $this; 93 | } 94 | 95 | /** 96 | * @param array $errors 97 | * 98 | * @return static 99 | */ 100 | public function setErrors(array $errors) 101 | { 102 | $this->errors = $errors; 103 | return $this; 104 | } 105 | 106 | /** 107 | * @param string $instance 108 | * 109 | * @return static 110 | */ 111 | public function setInstance($instance) 112 | { 113 | $this->instance = $instance; 114 | return $this; 115 | } 116 | 117 | /** 118 | * @return int 119 | */ 120 | public function getStatus() 121 | { 122 | return $this->status; 123 | } 124 | 125 | /** 126 | * @return string 127 | */ 128 | public function getDetail() 129 | { 130 | return $this->detail; 131 | } 132 | 133 | /** 134 | * @return string 135 | */ 136 | public function getType() 137 | { 138 | return $this->type; 139 | } 140 | 141 | /** 142 | * @return string 143 | */ 144 | public function getTitle() 145 | { 146 | return $this->title; 147 | } 148 | 149 | /** 150 | * @return string 151 | */ 152 | public function getInstance() 153 | { 154 | return $this->instance; 155 | } 156 | 157 | /** 158 | * @return 159 | */ 160 | public function getString() 161 | { 162 | return $this->string; 163 | } 164 | 165 | /** 166 | * @return array 167 | */ 168 | public function getErrors() 169 | { 170 | return $this->errors; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Provider/ConfigListProvider.php: -------------------------------------------------------------------------------- 1 | lists = $lists; 16 | $this->providerFactory = $providerFactory; 17 | } 18 | 19 | /** 20 | * Default list provider, retrieve the lists from the config file. 21 | * {@inheritdoc} 22 | */ 23 | public function getLists() 24 | { 25 | if (sizeof($this->lists) == 0) { 26 | throw new \RuntimeException("No Mailchimp list has been defined. Check the your config.yml file based on MailchimpBundle's README.md"); 27 | } 28 | $lists = array(); 29 | foreach ($this->lists as $listId => $listParameters) 30 | { 31 | $lists[] = $this->createList($listId, $listParameters); 32 | } 33 | return $lists; 34 | } 35 | 36 | /** 37 | * Default list provider, retrieve one list from the config file. 38 | * {@inheritdoc} 39 | */ 40 | public function getList($listId) 41 | { 42 | foreach ($this->lists as $id => $listParameters) 43 | { 44 | if($id == $listId) 45 | { 46 | return $this->createList($id, $listParameters); 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * create one SubscriberList 53 | * @param string $listId 54 | * @param string $listParameters 55 | * @return SubscriberList 56 | */ 57 | private function createList($listId, $listParameters) 58 | { 59 | $providerServiceKey = $listParameters['subscriber_provider']; 60 | $provider = $this->providerFactory->create($providerServiceKey); 61 | $subscriberList = new SubscriberList($listId, $provider, $listParameters['merge_fields']); 62 | $subscriberList->setWebhookUrl($listParameters['webhook_url']); 63 | $subscriberList->setWebhookSecret($listParameters['webhook_secret']); 64 | 65 | return $subscriberList; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Provider/DynamicProviderInterface.php: -------------------------------------------------------------------------------- 1 | userRepository = $userRepository; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getSubscribers() 31 | { 32 | $users = $this->userRepository->findSubscribers(); 33 | 34 | $subscribers = array_map(function (User $user) { 35 | $subscriber = new Subscriber($user->getEmail(), [ 36 | self::TAG_NICKNAME => $user->getNickname(), 37 | self::TAG_GENDER => $user->getGender(), 38 | self::TAG_BIRTHDATE => $user->getBirthdate() ? $user->getBirthdate()->format('Y-m-d') : null, 39 | self::TAG_CITY => $user->getCity(), 40 | self::TAG_LAST_ACTIVITY_DATE => $user->getLastActivityDate() ? $user->getLastActivityDate()->format('Y-m-d') : null, 41 | self::TAG_REGISTRATION_DATE => $user->getRegistrationDate() ? $user->getRegistrationDate()->format('Y-m-d') : null, 42 | ], [ 43 | 'language' => 'fr', 44 | 'email_type' => 'html' 45 | ]); 46 | 47 | return $subscriber; 48 | }, $users); 49 | 50 | return $subscribers; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Provider/FosSubscriberProvider.php: -------------------------------------------------------------------------------- 1 | userManager = $userManager; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function getSubscribers() 29 | { 30 | $users = $this->userManager->findUsers(); 31 | // or find only enabled users : 32 | // $users = $this->userManager->findUserBy(array('enabled' => true)); 33 | 34 | $subscribers = array_map(function (User $user) { 35 | $subscriber = new Subscriber($user->getEmail(), [ 36 | self::TAG_USERNAME => $user->getUsername(), 37 | self::TAG_ENABLED => $user->isEnabled(), 38 | self::TAG_LAST_LOGIN => $user->getLastLogin() ? $user->getLastLogin()->format('Y-m-d') : null 39 | ]); 40 | 41 | return $subscriber; 42 | }, $users); 43 | 44 | return $subscribers; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Provider/ListProviderInterface.php: -------------------------------------------------------------------------------- 1 | providers[$providerKey])) { 20 | $this->providers[$providerKey] = $provider; 21 | } 22 | } 23 | 24 | /** 25 | * Get subscriber provider. 26 | * 27 | * @param string $providerKey 28 | * 29 | * @return ProviderInterface $provider 30 | */ 31 | public function create($providerKey) 32 | { 33 | if (!isset($this->providers[$providerKey])) { 34 | throw new \InvalidArgumentException(sprintf('Provider "%s" should be defined as a service.', $providerKey)); 35 | } 36 | 37 | return $this->providers[$providerKey]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Provider/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | # mailchimp-bundle 2 | 3 | [![Build Status](https://travis-ci.org/welpdev/mailchimp-bundle.svg?branch=master)](https://travis-ci.org/welpdev/mailchimp-bundle) 4 | [![Packagist](https://img.shields.io/packagist/v/welp/mailchimp-bundle.svg)](https://packagist.org/packages/welp/mailchimp-bundle) 5 | [![Packagist](https://img.shields.io/packagist/dt/welp/mailchimp-bundle.svg)](https://packagist.org/packages/welp/mailchimp-bundle) 6 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/welpdev/mailchimp-bundle/master/LICENSE.md) 7 | 8 | This bundle will help you synchronise your project's newsletter subscribers into MailChimp through MailChimp API V3. 9 | 10 | ## Features 11 | 12 | * [x] Use your own userProvider (basic `FosSubscriberProvider` included to interface with FosUserBundle) 13 | * [x] Use your own listProvider (`DoctrineListProvider` included to retrieve your list from a database) 14 | * [x] Synchronize Merge Fields with your config 15 | * [x] Synchronize your subscriber with a List 16 | * [x] Use lifecycle event to subscribe/unsubscribe/delete subscriber from a List 17 | * [x] Retrieve [MailChimp Object](https://github.com/drewm/mailchimp-api) to make custom MailChimp API V3 requests 18 | * [x] Register Webhooks 19 | 20 | ## Setup 21 | 22 | Add bundle to your project: 23 | 24 | ```bash 25 | composer require welp/mailchimp-bundle 26 | ``` 27 | 28 | Add `Welp\MailchimpBundle\WelpMailchimpBundle` to your `AppKernel.php`: 29 | 30 | ```php 31 | $bundles = [ 32 | // ... 33 | new Welp\MailchimpBundle\WelpMailchimpBundle(), 34 | ]; 35 | ``` 36 | 37 | ## Minimal Configuration 38 | 39 | In your `config.yml`: 40 | 41 | ```yaml 42 | welp_mailchimp: 43 | api_key: YOURMAILCHIMPAPIKEY 44 | ``` 45 | 46 | More configuration on the [documentation](configuration.md). 47 | 48 | ## Documentation 49 | 50 | * [Setup](setup.md) 51 | * [Configuration](configuration.md) 52 | * [Subscriber Provider](subscriber-provider.md) 53 | * [List Provider](list-provider.md) 54 | * [Usage](usage.md) 55 | * Synchronize merge fields 56 | * Full synchronization with command 57 | * Unit synchronization with events 58 | * Subscribe new User 59 | * Unsubscribe a User 60 | * Update a User 61 | * Change User's email address (WORKAROUND) 62 | * Delete a User 63 | * Retrieve [MailChimp Object](https://github.com/drewm/mailchimp-api) to make custom MailChimp API V3 requests 64 | * [Webhook](webhook.md) 65 | * Update User when subscribe/unsubscribe 66 | 67 | ## Contributing 68 | 69 | If you want to contribute to this project, look at [over here](CONTRIBUTING.md) 70 | -------------------------------------------------------------------------------- /src/Resources/doc/list-provider.md: -------------------------------------------------------------------------------- 1 | # List Provider 2 | 3 | By default the list configuration is read from the `config.yml` when correctly defined according to [configuring your lists](configuration.md). This is done by the default list provider (`ConfigListProvider`). If you want to use a diffrent source for your list config you can create your own List Provider that should implement `Welp\MailChimpBundle\Provider\ListProviderInterface`. 4 | 5 | ```php 6 | em = $entityManager; 51 | $this->listEntity = $listEntity; 52 | $this->subscriberProvider = $subscriberProvider; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getList($listId) 59 | { 60 | $list = $this->em->getRepository($this->listEntity)->findOneByListId($listId); 61 | $list->setProvider($this->subscriberProvider); 62 | return $list; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function getLists() 69 | { 70 | $lists = $this->em->getRepository($this->listEntity)->findAll(); 71 | foreach($lists as $list) 72 | { 73 | //add the provider to the list 74 | $list->setProvider($this->subscriberProvider); 75 | } 76 | return $lists; 77 | } 78 | } 79 | 80 | ``` 81 | *Got your SubscriberProvider service key saved as a string with your list config ?
82 | Make use of the `ProviderFactory` to get the the service:
83 | `$provider = $this->providerFactory->create($providerServiceKey);`* 84 | 85 | Define your List provider as a service: 86 | 87 | ```yaml 88 | doctrine.list.provider: 89 | class: YourApp\App\Provider\DoctrineListProvider 90 | public: true 91 | arguments: 92 | - '@doctrine.orm.entity_manager' 93 | - 'YourApp\App\Entity\SubscriberList' 94 | - '@example_user_provider' 95 | ``` 96 | 97 | After this, don't forget to add the service key for your provider into your `config.yml`: 98 | 99 | ```yaml 100 | welp_mailchimp: 101 | api_key: ... 102 | list_provider: 'doctrine.list.provider' 103 | ... 104 | ``` 105 | -------------------------------------------------------------------------------- /src/Resources/doc/mailchimp-doc.md: -------------------------------------------------------------------------------- 1 | MailChimp Documentation and usefull links 2 | 3 | * 4 | * 5 | * 6 | * 7 | * 8 | -------------------------------------------------------------------------------- /src/Resources/doc/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Installation 4 | 5 | Add bundle to your project: 6 | 7 | ```bash 8 | composer require welp/mailchimp-bundle 9 | ``` 10 | 11 | Add `Welp\MailchimpBundle\WelpMailchimpBundle` to your `AppKernel.php`: 12 | 13 | ```php 14 | $bundles = [ 15 | // ... 16 | new Welp\MailchimpBundle\WelpMailchimpBundle(), 17 | ]; 18 | ``` 19 | 20 | ## Minimal Configuration 21 | 22 | In your `config.yml`: 23 | 24 | ```yaml 25 | welp_mailchimp: 26 | api_key: YOURMAILCHIMPAPIKEY 27 | ``` 28 | 29 | [More configuration](configuration.md) 30 | -------------------------------------------------------------------------------- /src/Resources/doc/subscriber-language.md: -------------------------------------------------------------------------------- 1 | # Subscriber Language 2 | 3 | * [MailChimp Doc](http://kb.mailchimp.com/lists/managing-subscribers/view-and-edit-subscriber-languages) 4 | 5 | ## Language codes 6 | 7 | * English = en 8 | * Arabic = ar 9 | * Afrikaans = af 10 | * Belarusian = be 11 | * Bulgarian = bg 12 | * Catalan = ca 13 | * Chinese = zh 14 | * Croatian = hr 15 | * Czech = cs 16 | * Danish = da 17 | * Dutch = nl 18 | * Estonian = et 19 | * Farsi = fa 20 | * Finnish = fi 21 | * French (France) = fr 22 | * French (Canada) = fr_CA 23 | * German = de 24 | * Greek = el 25 | * Hebrew = he 26 | * Hindi = hi 27 | * Hungarian = hu 28 | * Icelandic = is 29 | * Indonesian = id 30 | * Irish = ga 31 | * Italian = it 32 | * Japanese = ja 33 | * Khmer = km 34 | * Korean = ko 35 | * Latvian = lv 36 | * Lithuanian = lt 37 | * Maltese = mt 38 | * Malay = ms 39 | * Macedonian = mk 40 | * Norwegian = no 41 | * Polish = pl 42 | * Portuguese (Brazil) = pt 43 | * Portuguese (Portugal) = pt_PT 44 | * Romanian = ro 45 | * Russian = ru 46 | * Serbian = sr 47 | * Slovak = sk 48 | * Slovenian = sl 49 | * Spanish (Mexico) = es 50 | * Spanish (Spain) = es_ES 51 | * Swahili = sw 52 | * Swedish = sv 53 | * Tamil = ta 54 | * Thai = th 55 | * Turkish = tr 56 | * Ukrainian = uk 57 | * Vietnamese = vi 58 | -------------------------------------------------------------------------------- /src/Resources/doc/subscriber-provider.md: -------------------------------------------------------------------------------- 1 | # Subscriber Provider 2 | 3 | After [configuring your lists](configuration.md) in `config.yml`, you need to create at least one `Provider` that will be used by the Symfony command. Your provider should be accessible via a service key (the same you reference in `subscriber_provider` in the configuration above): 4 | 5 | ```yaml 6 | services: 7 | yourapp_mailchimp_subscriber_provider: 8 | class: YourApp\App\Newsletter\SubscriberProvider 9 | arguments: [@yourapp_user_repository] 10 | ``` 11 | 12 | It should implement `Welp\MailChimpBundle\Provider\ProviderInterface` and return an array of `Welp\MailChimpBundle\Subscriber\Subscriber` objects. The first argument of the `Subscriber` object is its e-mail, the second argument is an array of merge fields values you need to add in MailChimp's backend in your list settings under `List fields and *|MERGE|* tags` (see this [guide on MailChimp](http://kb.mailchimp.com/merge-tags/using/getting-started-with-merge-tags) to add merge tags in your list) and the third is an array of options [See MailChimp Documentation](http://developer.mailchimp.com/documentation/mailchimp/reference/lists/members/). 13 | 14 | ```php 15 | userRepository = $userRepository; 39 | } 40 | 41 | public function getSubscribers() 42 | { 43 | $users = $this->userRepository->findSubscribers(); 44 | 45 | $subscribers = array_map(function(User $user) { 46 | $subscriber = new Subscriber($user->getEmail(), [ 47 | self::TAG_NICKNAME => $user->getNickname(), 48 | self::TAG_GENDER => $user->getGender(), 49 | self::TAG_BIRTHDATE => $user->getBirthdate() ? $user->getBirthdate()->format('Y-m-d') : null, 50 | self::TAG_CITY => $user->getCity(), 51 | self::TAG_LAST_ACTIVITY_DATE => $user->getLastActivityDate() ? $user->getLastActivityDate()->format('Y-m-d') : null, 52 | self::TAG_REGISTRATION_DATE => $user->getRegistrationDate() ? $user->getRegistrationDate()->format('Y-m-d') : null, 53 | ],[ 54 | 'language' => 'fr', 55 | 'email_type' => 'html' 56 | ]); 57 | 58 | return $subscriber; 59 | }, $users); 60 | 61 | return $subscribers; 62 | } 63 | } 64 | 65 | ``` 66 | 67 | We also provide a ready to use provider for the FosUserBundle -> FosSubscriberProvider. You just need to register the service into your app: 68 | 69 | ```yaml 70 | services: 71 | yourapp_mailchimp_fos_subscriber_provider: 72 | class: Welp\MailchimpBundle\Provider\FosSubscriberProvider 73 | arguments: [@fos_user.user_manager] 74 | ``` 75 | 76 | After this, don't forget to add the service key for your list into your `config.yml`: 77 | 78 | ```yaml 79 | ... 80 | listId2: 81 | subscriber_provider: 'yourapp.provider2' 82 | ... 83 | ``` 84 | 85 | ## Dynamic Subscriber Provider 86 | 87 | If you want to create a reusable provider for multiple list without defining a service for each one, you can make use of the `Welp\MailChimpBundle\Provider\DynamicProviderInterface`. It works exactly the same as the `ProviderInterface` except it has one setter method that is used to set the current list in the synchronisation process. 88 | 89 | Example implementation: 90 | 91 | ```php 92 | userRepository = $userRepository; 118 | } 119 | 120 | // List id is set for each list in the synchronisation process 121 | public function setListId(string $listId) 122 | { 123 | $this->listId = $listId; 124 | } 125 | 126 | public function getSubscribers() 127 | { 128 | //now make use of the list id when querying for users 129 | $users = $this->userRepository->findSubscribersByListId($this->listId); 130 | 131 | $subscribers = array_map(function(User $user) { 132 | $subscriber = new Subscriber($user->getEmail(), [ 133 | self::TAG_NICKNAME => $user->getNickname(), 134 | self::TAG_GENDER => $user->getGender(), 135 | self::TAG_BIRTHDATE => $user->getBirthdate() ? $user->getBirthdate()->format('Y-m-d') : null, 136 | self::TAG_CITY => $user->getCity(), 137 | self::TAG_LAST_ACTIVITY_DATE => $user->getLastActivityDate() ? $user->getLastActivityDate()->format('Y-m-d') : null, 138 | self::TAG_REGISTRATION_DATE => $user->getRegistrationDate() ? $user->getRegistrationDate()->format('Y-m-d') : null, 139 | ],[ 140 | 'language' => 'fr', 141 | 'email_type' => 'html' 142 | ]); 143 | 144 | return $subscriber; 145 | }, $users); 146 | 147 | return $subscribers; 148 | } 149 | } 150 | 151 | 152 | ``` -------------------------------------------------------------------------------- /src/Resources/doc/tests.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | * 4 | * 5 | 6 | ## Unit tests 7 | 8 | bin/phpspec run 9 | 10 | ## Integration tests 11 | 12 | bin/phpunit -c . 13 | 14 | Look at the code, it works with a MailChimp account... 15 | 16 | ## Webhook tests 17 | 18 | use [ngrok](https://ngrok.com/) and 19 | 20 | ## Debug/dev mode 21 | 22 | The best solution to debug or dev environment is to create a free MailChimp account and use the API key of this free "debug/dev" MailChimp account. 23 | 24 | We are waiting for a sandbox from MailChimp developer in order to implements a pretty debug/dev mode. 25 | -------------------------------------------------------------------------------- /src/Resources/doc/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Synchronize merge fields 4 | 5 | Merge fields are values you can add to your subscribers (for example the firstname or birthdate of your user). You can then use these tags in your newsletters or create segments out of them. 6 | 7 | To learn more about merge tags, please see this [guide on MailChimp](http://kb.mailchimp.com/merge-tags/using/getting-started-with-merge-tags). 8 | 9 | To synchronize you need to create your **lists** in MailChimp backend first. Then you need to add them in your `config.yml` as shown in the [above configuration](configuration.md). The `options` you can provide are the same as the one found in [MailChimp API](http://developer.mailchimp.com/documentation/mailchimp/reference/lists/merge-fields/). 10 | 11 | You can then synchronize the merge fields using the `php app/console welp:mailchimp:synchronize-merge-fields` command. Note that every tag that are present in MailChimp but are not defined in your configuration **will be deleted along with associated values**. 12 | 13 | NOTE: MailChimp provide two default merge fields: 14 | 15 | * FNAME: Firstname 16 | * LNAME: Lastname 17 | 18 | **Known issues with MailChimp API V3 - 17/12/2016** 19 | 20 | * When you try to synchronize merge fields with choice type, the API doesn't handle update of choice fields... It returns an error 400. Workaround: you have to remove all your choice type merge fields from your list throught the MailChimp Admin panel and retry synchronize with the command. 21 | 22 | ## Full synchronization with command 23 | 24 | You can synchronize all subscribers of your project at once by calling the Symfony command `php app/console welp:mailchimp:synchronize-subscribers`. 25 | You can use the option `--follow-sync` to follow batches executions. 26 | 27 | It will first fetch all the subscribers already present in MailChimp and unsubscribe any subscribers that are not in your projet (they might have been deleted on the project side), it will then send all your subscribers to MailChimp, new subscribers will be added and existing subscribers will be updated. 28 | 29 | NOTE: you must have configure and create your own [subscriber provider](subscriber-provider.md). 30 | 31 | ## Unit synchronization with events 32 | 33 | If you want realtime synchronization, you can dispatch custom events on your controllers (or anywhere). The subscribe event can be used both for adding a new subscriber or updating an existing one. You can fired these events to trigger sync with MailChimp: 34 | 35 | SubscriberEvent::EVENT_SUBSCRIBE = 'welp.mailchimp.subscribe'; 36 | SubscriberEvent::EVENT_UNSUBSCRIBE = 'welp.mailchimp.unsubscribe'; 37 | SubscriberEvent::EVENT_PENDING = 'welp.mailchimp.pending'; 38 | SubscriberEvent::EVENT_CLEAN = 'welp.mailchimp.clean'; 39 | SubscriberEvent::EVENT_UPDATE = 'welp.mailchimp.update'; 40 | SubscriberEvent::EVENT_CHANGE_EMAIL = 'welp.mailchimp.change_email'; 41 | SubscriberEvent::EVENT_DELETE = 'welp.mailchimp.delete'; 42 | 43 | ### Subscribe new User 44 | 45 | Here is an example of a subscribe event dispatch: 46 | 47 | ```php 48 | getEmail(), [ 60 | 'FIRSTNAME' => $user->getFirstname(), 61 | ], [ 62 | 'language' => 'fr' 63 | ]); 64 | 65 | $this->container->get('event_dispatcher')->dispatch( 66 | SubscriberEvent::EVENT_SUBSCRIBE, 67 | new SubscriberEvent('your_list_id', $subscriber) 68 | ); 69 | } 70 | ``` 71 | 72 | ### Unsubscribe a User 73 | 74 | Here is an example of unsubscribe event dispatch: 75 | 76 | ```php 77 | getEmail()); 89 | 90 | $this->container->get('event_dispatcher')->dispatch( 91 | SubscriberEvent::EVENT_UNSUBSCRIBE, 92 | new SubscriberEvent('your_list_id', $subscriber) 93 | ); 94 | } 95 | ``` 96 | 97 | ### Pending a User 98 | 99 | Here is an example of pending event dispatch: 100 | 101 | ```php 102 | getEmail(), [ 114 | 'FIRSTNAME' => $user->getFirstname(), 115 | ], [ 116 | 'language' => 'fr' 117 | ]); 118 | 119 | $this->container->get('event_dispatcher')->dispatch( 120 | SubscriberEvent::EVENT_PENDING, 121 | new SubscriberEvent('your_list_id', $subscriber) 122 | ); 123 | } 124 | ``` 125 | 126 | ### Clean a User 127 | 128 | Here is an example of clean event dispatch: 129 | 130 | ```php 131 | getEmail(), [ 143 | 'FIRSTNAME' => $user->getFirstname(), 144 | ], [ 145 | 'language' => 'fr' 146 | ]); 147 | 148 | $this->container->get('event_dispatcher')->dispatch( 149 | SubscriberEvent::EVENT_CLEAN, 150 | new SubscriberEvent('your_list_id', $subscriber) 151 | ); 152 | } 153 | ``` 154 | 155 | ### Update a User 156 | 157 | If your User changes his information, you can sync with MailChimp: 158 | 159 | ```php 160 | getEmail(), [ 172 | 'FIRSTNAME' => $user->getFirstname(), 173 | 'LASTNAME' => $user->getFirstname(), 174 | ], [ 175 | 'language' => 'en' 176 | ]); 177 | 178 | $this->container->get('event_dispatcher')->dispatch( 179 | SubscriberEvent::EVENT_UPDATE, 180 | new SubscriberEvent('your_list_id', $subscriber) 181 | ); 182 | } 183 | ``` 184 | 185 | Note: we can't change the address email of a user... It's a MailChimp API V3 [issue](http://stackoverflow.com/questions/32224697/mailchimp-api-v3-0-change-subscriber-email?noredirect=1&lq=1). I already have contacted MailChimp, but you can contact them too. 186 | 187 | The only workaround right now to change user address mail is to delete the old member and add a new one... 188 | 189 | ### Change User's email address (WORKAROUND) 190 | 191 | If your User changes his email address, you can sync with MailChimp: 192 | 193 | ```php 194 | container->get('event_dispatcher')->dispatch( 204 | SubscriberEvent::EVENT_CHANGE_EMAIL, 205 | new SubscriberEvent('your_list_id', $subscriber, $oldEmail) 206 | ); 207 | 208 | } 209 | ``` 210 | 211 | It's an UGLY WORKAROUND... Waiting for MailChimp API V3 to be able to properly change email address of a member... 212 | 213 | ### Delete a User 214 | 215 | And finally delete a User 216 | 217 | Unsubscribe is simpler, you only need the email, all merge fields will be ignored: 218 | 219 | ```php 220 | getEmail()); 232 | 233 | $this->container->get('event_dispatcher')->dispatch( 234 | SubscriberEvent::EVENT_DELETE, 235 | new SubscriberEvent('your_list_id', $subscriber) 236 | ); 237 | } 238 | ``` 239 | 240 | ## Retrieve MailChimp Object to make custom MailChimp API V3 requests 241 | 242 | You can also retrieve the MailChimp Object which comes from the library [drewm/mailchimp-api](https://github.com/drewm/mailchimp-api). 243 | 244 | The service key is `welp_mailchimp.mailchimp_master`. 245 | 246 | Example: 247 | 248 | ``` php 249 | container->get('welp_mailchimp.mailchimp_master'); 253 | $list_id = 'b1234346'; 254 | 255 | $result = $MailChimp->post("lists/$list_id/members", [ 256 | 'email_address' => 'davy@example.com', 257 | 'status' => 'subscribed', 258 | ]); 259 | 260 | if ($MailChimp->success()) { 261 | print_r($result); 262 | } else { 263 | echo $MailChimp->getLastError(); 264 | } 265 | 266 | ``` 267 | -------------------------------------------------------------------------------- /src/Resources/doc/webhook.md: -------------------------------------------------------------------------------- 1 | # Webhook 2 | 3 | * [Documentation](https://apidocs.mailchimp.com/webhooks/) 4 | * [How to set up webhooks](http://kb.mailchimp.com/integrations/api-integrations/how-to-set-up-webhooks) 5 | * [Webhooks API V3](http://developer.mailchimp.com/documentation/mailchimp/reference/lists/webhooks/) 6 | 7 | Webhooks will be triggered when an event occured in MailChimp, and it will call our webhook url and fired Webhook Events in our Symfony App. We will listen to these events in order to add our logic/workflow. 8 | 9 | ## Configuration 10 | 11 | You need to add the webhook routing to your app routing: 12 | 13 | `app/routing.yml` 14 | 15 | myapp_mailchimp_webhook: 16 | resource: "@WelpMailchimpBundle/Resources/config/routing.yml" 17 | prefix: /mailchimp 18 | 19 | Note: you can change the prefix as you like. 20 | 21 | This will generate an url to the webhook like this: 22 | 23 | Also, MailChimp recommand to protect webhook url with a token parameter. So you need to add the secret token to your list in your config.yml 24 | 25 | `config.yml` 26 | 27 | welp_mailchimp: 28 | api_key: 3419ca97412af7c2893b89894275b415-us14 29 | lists: 30 | ba039c6198: 31 | webhook_secret: thisisTheSecretPass 32 | ... 33 | 34 | Note: To access properly to the webhook function you will have to use the url with the secret parameter: 35 | 36 | ## Register the webhook manually 37 | 38 | See [How to set up webhooks](http://kb.mailchimp.com/integrations/api-integrations/how-to-set-up-webhooks). 39 | 40 | And the webhook url you have to register is: 41 | 42 | 43 | ## Command to automatically register webhook to lists 44 | 45 | There is a command to automatically register webhook to lists 46 | 47 | Before using it, you have to add the webhook_url into lists in `config.yml` 48 | 49 | `config.yml` 50 | 51 | welp_mailchimp: 52 | api_key: 3419ca97412af7c2893b89894275b415-us14 53 | lists: 54 | ba039c6198: 55 | webhook_secret: thisisTheSecretPass 56 | webhook_url: http://domain.com/mailchimp/webhook/endpoint 57 | 58 | Next in your terminal use this command `php app/console welp:mailchimp:webhook`. You can verify in your MailChimp List that the webhook has been added. 59 | 60 | ## Events to listen 61 | 62 | In order to integrate MailChimp into your app workflow, you can listen to different Event. 63 | 64 | Event you can listen: 65 | 66 | WebhookEvent::EVENT_SUBSCRIBE = 'welp.mailchimp.webhook.subscribe'; 67 | WebhookEvent::EVENT_UNSUBSCRIBE = 'welp.mailchimp.webhook.unsubscribe'; 68 | WebhookEvent::EVENT_PROFILE = 'welp.mailchimp.webhook.profile'; 69 | WebhookEvent::EVENT_CLEANED = 'welp.mailchimp.webhook.cleaned'; 70 | WebhookEvent::EVENT_UPEMAIL = 'welp.mailchimp.webhook.upemail'; 71 | WebhookEvent::EVENT_CAMPAIGN = 'welp.mailchimp.webhook.campaign'; 72 | 73 | Example: 74 | 75 | ### 1- Create listener 76 | 77 | ``` php 78 | container = $container; 97 | } 98 | 99 | public static function getSubscribedEvents() 100 | { 101 | return [ 102 | WebhookEvent::EVENT_SUBSCRIBE => 'subscribe', 103 | WebhookEvent::EVENT_UNSUBSCRIBE => 'unsubscribe', 104 | WebhookEvent::EVENT_PROFILE => 'profile', 105 | WebhookEvent::EVENT_CLEANED => 'cleaned', 106 | WebhookEvent::EVENT_UPEMAIL => 'upemail', 107 | WebhookEvent::EVENT_CAMPAIGN => 'campaign' 108 | ]; 109 | } 110 | 111 | public function subscribe(WebhookEvent $event){ 112 | $logger = $this->container->get('logger'); 113 | $logger->info('Subscribe Event:', $event->getData()); 114 | } 115 | 116 | public function unsubscribe(WebhookEvent $event){ 117 | $logger = $this->container->get('logger'); 118 | $logger->info('Unsubscribe Event:', $event->getData()); 119 | } 120 | 121 | public function profile(WebhookEvent $event){ 122 | $logger = $this->container->get('logger'); 123 | $logger->info('Profile Event:', $event->getData()); 124 | } 125 | 126 | public function cleaned(WebhookEvent $event){ 127 | $logger = $this->container->get('logger'); 128 | $logger->info('Cleaned Event:', $event->getData()); 129 | } 130 | 131 | public function upemail(WebhookEvent $event){ 132 | $logger = $this->container->get('logger'); 133 | $logger->info('Upemail Event:', $event->getData()); 134 | } 135 | 136 | public function campaign(WebhookEvent $event){ 137 | $logger = $this->container->get('logger'); 138 | $logger->info('campaign Event:', $event->getData()); 139 | } 140 | 141 | 142 | } 143 | ``` 144 | 145 | ### 2- Register the listener into services.yml 146 | 147 | services: 148 | app.listener.mailchimp.webhook: 149 | class: AppBundle\Listener\MailchimpEventListener 150 | tags: 151 | - { name: kernel.event_subscriber } 152 | arguments: 153 | - @service_container 154 | 155 | ### 3- Test with ngrok (or other localhost tunnel) and you will see the result in app log: 156 | 157 | ... 158 | [2016-09-05 11:55:48] app.INFO: Unsubscribe Event: {"reason":"manual","id":"5c1b5a7c1e","email":"tztz@gmail.com","email_type":"html","web_id":"3375995","merges":{"EMAIL":"tztz@gmail.com","FNAME":"Tztz","LNAME":"TZST"},"list_id":"ba039c6198"} [] 159 | -------------------------------------------------------------------------------- /src/Subscriber/ListRepository.php: -------------------------------------------------------------------------------- 1 | mailchimp = $mailchimp; 38 | } 39 | 40 | /** 41 | * Get MailChimp Object to do custom actions 42 | * @return MailChimp https://github.com/drewm/mailchimp-api 43 | */ 44 | public function getMailChimp() 45 | { 46 | return $this->mailchimp; 47 | } 48 | 49 | /** 50 | * Find MailChimp List by list Id 51 | * @param string $listId 52 | * @return array list http://developer.mailchimp.com/documentation/mailchimp/reference/lists/#read-get_lists_list_id 53 | */ 54 | public function findById($listId) 55 | { 56 | $listData = $this->mailchimp->get("lists/$listId"); 57 | 58 | if (!$this->mailchimp->success()) { 59 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 60 | } 61 | return $listData; 62 | } 63 | 64 | /** 65 | * core function to put (add or edit) subscriber to a list 66 | * @param string $listId 67 | * @param Subscriber $subscriber 68 | * @param string $status 69 | * @return array 70 | */ 71 | protected function putSubscriberInList($listId, Subscriber $subscriber, $status) 72 | { 73 | if (!in_array($status, ['subscribed', 'unsubscribed', 'cleaned', 'pending', 'transactional'])) { 74 | throw new \RuntimeException('$status must be one of these values: subscribed, unsubscribed, cleaned, pending, transactional'); 75 | } 76 | $subscriberHash = MailChimp::subscriberHash($subscriber->getEmail()); 77 | $result = $this->mailchimp->put("lists/$listId/members/$subscriberHash", 78 | array_merge( 79 | $subscriber->formatMailChimp(), 80 | ['status' => $status] 81 | ) 82 | ); 83 | 84 | if (!$this->mailchimp->success()) { 85 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | /** 92 | * Subscribe a Subscriber to a list 93 | * @param string $listId 94 | * @param Subscriber $subscriber 95 | * @return array 96 | */ 97 | public function subscribe($listId, Subscriber $subscriber) 98 | { 99 | return $this->putSubscriberInList($listId, $subscriber, 'subscribed'); 100 | } 101 | 102 | /** 103 | * Subscribe a Subscriber to a list 104 | * @param string $listId 105 | * @param Subscriber $subscriber 106 | */ 107 | public function unsubscribe($listId, Subscriber $subscriber) 108 | { 109 | return $this->putSubscriberInList($listId, $subscriber, 'unsubscribed'); 110 | } 111 | 112 | /** 113 | * Clean a Subscriber to a list 114 | * @param string $listId 115 | * @param Subscriber $subscriber 116 | */ 117 | public function clean($listId, Subscriber $subscriber) 118 | { 119 | return $this->putSubscriberInList($listId, $subscriber, 'cleaned'); 120 | } 121 | 122 | /** 123 | * Add/set pending a Subscriber to a list 124 | * @param string $listId 125 | * @param Subscriber $subscriber 126 | */ 127 | public function pending($listId, Subscriber $subscriber) 128 | { 129 | return $this->putSubscriberInList($listId, $subscriber, 'pending'); 130 | } 131 | 132 | /** 133 | * set transactional a Subscriber to a list 134 | * @param string $listId 135 | * @param Subscriber $subscriber 136 | */ 137 | public function transactional($listId, Subscriber $subscriber) 138 | { 139 | return $this->putSubscriberInList($listId, $subscriber, 'transactional'); 140 | } 141 | 142 | /** 143 | * Update a Subscriber to a list 144 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/members/#edit-patch_lists_list_id_members_subscriber_hash 145 | * @param string $listId 146 | * @param Subscriber $subscriber 147 | */ 148 | public function update($listId, Subscriber $subscriber) 149 | { 150 | $subscriberHash = MailChimp::subscriberHash($subscriber->getEmail()); 151 | $result = $this->mailchimp->patch("lists/$listId/members/$subscriberHash", $subscriber->formatMailChimp()); 152 | 153 | if (!$this->mailchimp->success()) { 154 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 155 | } 156 | 157 | return $result; 158 | } 159 | 160 | /** 161 | * TODO not working with API V3... we can't change email of a user 162 | * one possible solution is to delete old subscriber and add a new one 163 | * with the same mergeFieds and Options... 164 | * Change email address 165 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/members/#edit-put_lists_list_id_members_subscriber_hash 166 | * @param string $listId 167 | * @param Subscriber $newSubscriber 168 | * @param string $oldEmailAddress 169 | */ 170 | public function changeEmailAddress($listId, Subscriber $newSubscriber, $oldEmailAddress) 171 | { 172 | # @NOTE handle special cases: 173 | # 1. new email address already exists in List 174 | # 2. old email address not exists in list 175 | # 3. old or new email address doesn't exists in list 176 | 177 | $subscriberHash = MailChimp::subscriberHash($oldEmailAddress); 178 | $oldMember = $this->mailchimp->get("lists/$listId/members/$subscriberHash"); 179 | if (!$this->mailchimp->success()) { 180 | // problem with the oldSubcriber (doest not exist, ...) 181 | // np we will take the new Subscriber 182 | $newMember = $newSubscriber->formatMailChimp(); 183 | $newMember['status_if_new'] = 'subscribed'; 184 | } else { 185 | // clean member 186 | unset($oldMember['_links']); 187 | unset($oldMember['id']); 188 | unset($oldMember['stats']); 189 | unset($oldMember['unique_email_id']); 190 | unset($oldMember['member_rating']); 191 | unset($oldMember['last_changed']); 192 | unset($oldMember['email_client']); 193 | unset($oldMember['list_id']); 194 | 195 | $newMember = $oldMember; 196 | $newMember['email_address'] = $newSubscriber->getEmail(); 197 | $newMember['status_if_new'] = 'subscribed'; 198 | 199 | // delete the old Member 200 | $deleteOld = $this->mailchimp->delete("lists/$listId/members/$subscriberHash"); 201 | if (!$this->mailchimp->success()) { 202 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 203 | } 204 | } 205 | 206 | // add/update the new member 207 | $subscriberHash = MailChimp::subscriberHash($newSubscriber->getEmail()); 208 | $result = $this->mailchimp->put("lists/$listId/members/$subscriberHash", $newMember); 209 | if (!$this->mailchimp->success()) { 210 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 211 | } 212 | 213 | return $result; 214 | } 215 | 216 | /** 217 | * Delete a Subscriber to a list 218 | * @param string $listId 219 | * @param Subscriber $subscriber 220 | */ 221 | public function delete($listId, Subscriber $subscriber) 222 | { 223 | $subscriberHash = MailChimp::subscriberHash($subscriber->getEmail()); 224 | $result = $this->mailchimp->delete("lists/$listId/members/$subscriberHash"); 225 | 226 | if (!$this->mailchimp->success()) { 227 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 228 | } 229 | 230 | return $result; 231 | } 232 | 233 | /** 234 | * Subscribe a batch of Subscriber to a list 235 | * @param string $listId 236 | * @param array $subscribers 237 | * @return array $batchIds 238 | */ 239 | public function batchSubscribe($listId, array $subscribers) 240 | { 241 | $batchResults = []; 242 | // as suggested in MailChimp API docs, we send multiple smaller requests instead of a bigger one 243 | $subscriberChunks = array_chunk($subscribers, self::SUBSCRIBER_BATCH_SIZE); 244 | foreach ($subscriberChunks as $subscriberChunk) { 245 | $Batch = $this->mailchimp->new_batch(); 246 | foreach ($subscriberChunk as $index => $newsubscribers) { 247 | $subscriberHash = MailChimp::subscriberHash($newsubscribers->getEmail()); 248 | $Batch->put("op$index", "lists/$listId/members/$subscriberHash", array_merge( 249 | $newsubscribers->formatMailChimp(), 250 | ['status' => 'subscribed'] 251 | )); 252 | } 253 | $Batch->execute(); 254 | $currentBatch = $Batch->check_status(); 255 | array_push($batchResults, $currentBatch); 256 | } 257 | return $batchResults; 258 | } 259 | 260 | /** 261 | * Unsubscribe a batch of Subscriber to a list 262 | * @param string $listId 263 | * @param array $emails 264 | * @return array $batchIds 265 | */ 266 | public function batchUnsubscribe($listId, array $emails) 267 | { 268 | $batchIds = []; 269 | // as suggested in MailChimp API docs, we send multiple smaller requests instead of a bigger one 270 | $emailsChunks = array_chunk($emails, self::SUBSCRIBER_BATCH_SIZE); 271 | foreach ($emailsChunks as $emailsChunk) { 272 | $Batch = $this->mailchimp->new_batch(); 273 | foreach ($emailsChunk as $index => $email) { 274 | $emailHash = MailChimp::subscriberHash($email); 275 | $Batch->patch("op$index", "lists/$listId/members/$emailHash", [ 276 | 'status' => 'unsubscribed' 277 | ]); 278 | } 279 | $result = $Batch->execute(); 280 | $currentBatch = $Batch->check_status(); 281 | array_push($batchIds, $currentBatch['id']); 282 | } 283 | return $batchIds; 284 | } 285 | 286 | /** 287 | * Delete a batch of Subscriber to a list 288 | * @param string $listId 289 | * @param array $emails 290 | * @return array $batchIds 291 | */ 292 | public function batchDelete($listId, array $emails) 293 | { 294 | $batchIds = []; 295 | // as suggested in MailChimp API docs, we send multiple smaller requests instead of a bigger one 296 | $emailsChunks = array_chunk($emails, self::SUBSCRIBER_BATCH_SIZE); 297 | foreach ($emailsChunks as $emailsChunk) { 298 | $Batch = $this->mailchimp->new_batch(); 299 | foreach ($emailsChunk as $index => $email) { 300 | $emailHash = MailChimp::subscriberHash($email); 301 | $Batch->delete("op$index", "lists/$listId/members/$emailHash"); 302 | } 303 | $result = $Batch->execute(); 304 | $currentBatch = $Batch->check_status(); 305 | array_push($batchIds, $currentBatch['id']); 306 | } 307 | return $batchIds; 308 | } 309 | 310 | /** 311 | * Get Members of a list 312 | * @param string $listId 313 | * @return array 314 | */ 315 | public function getMembers($listId) 316 | { 317 | $emails = []; 318 | $result = $this->mailchimp->get("lists/$listId/members"); 319 | 320 | if (!$this->mailchimp->success()) { 321 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 322 | } 323 | 324 | return $result; 325 | } 326 | 327 | /** 328 | * Get an array of subscribers emails from a list 329 | * @param string $listId 330 | * @return array 331 | */ 332 | public function getSubscriberEmails($listId) 333 | { 334 | $emails = []; 335 | $members = []; 336 | $offset=0; 337 | $maxresult = 200; 338 | $result = $this->mailchimp->get("lists/$listId/members", ['count'=> $maxresult]); 339 | 340 | if (!$this->mailchimp->success()) { 341 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 342 | } 343 | 344 | $totalItems = $result['total_items']; 345 | $members = array_merge($members, $result['members']); 346 | 347 | while ($offset < $totalItems) { 348 | $offset+=$maxresult; 349 | $result = $this->mailchimp->get("lists/$listId/members", [ 350 | 'count' => $maxresult, 351 | 'offset' => $offset 352 | ]); 353 | 354 | if (!$this->mailchimp->success()) { 355 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 356 | } 357 | $members = array_merge($members, $result['members']); 358 | }; 359 | 360 | foreach ($members as $key => $member) { 361 | array_push($emails, $member['email_address']); 362 | } 363 | 364 | return $emails; 365 | } 366 | 367 | /** 368 | * find all merge fields for a list 369 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/merge-fields/# 370 | * @param string $listId 371 | * @return array 372 | */ 373 | public function getMergeFields($listId) 374 | { 375 | $result = $this->mailchimp->get("lists/$listId/merge-fields"); 376 | 377 | # Handle mailchimp default count limit 378 | if ($result['total_items'] > self::MAILCHIMP_DEFAULT_COUNT_LIMIT) { 379 | $result = $this->mailchimp->get("lists/$listId/merge-fields", array("count" => $result['total_items'])); 380 | } 381 | 382 | if (!$this->mailchimp->success()) { 383 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 384 | } 385 | 386 | return $result['merge_fields']; 387 | } 388 | 389 | /** 390 | * add merge field for a list 391 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/merge-fields/# 392 | * @param string $listId 393 | * @param array $mergeData ["name" => '', "type" => ''] 394 | * @return array 395 | */ 396 | public function addMergeField($listId, array $mergeData) 397 | { 398 | $result = $this->mailchimp->post("lists/$listId/merge-fields", $mergeData); 399 | 400 | if (!$this->mailchimp->success()) { 401 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 402 | } 403 | 404 | return $result; 405 | } 406 | 407 | /** 408 | * add merge field for a list 409 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/merge-fields/#edit-patch_lists_list_id_merge_fields_merge_id 410 | * @param string $listId 411 | * @param string $mergeId 412 | * @param array $mergeData ["name" => '', "type" => '', ...] 413 | * @return array 414 | */ 415 | public function updateMergeField($listId, $mergeId, $mergeData) 416 | { 417 | $result = $this->mailchimp->patch("lists/$listId/merge-fields/$mergeId", $mergeData); 418 | 419 | if (!$this->mailchimp->success()) { 420 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 421 | } 422 | 423 | return $result; 424 | } 425 | 426 | /** 427 | * delete merge field for a list 428 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/merge-fields/# 429 | * @param string $listId 430 | * @param string $mergeId 431 | * @return array 432 | */ 433 | public function deleteMergeField($listId, $mergeId) 434 | { 435 | $result = $this->mailchimp->delete("lists/$listId/merge-fields/$mergeId"); 436 | 437 | if (!$this->mailchimp->success()) { 438 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 439 | } 440 | 441 | return $result; 442 | } 443 | 444 | /** 445 | * Automatically configure Webhook for a list 446 | * @param string $listId 447 | * @param string $webhookurl 448 | * @return array 449 | */ 450 | public function registerMainWebhook($listId, $webhookurl) 451 | { 452 | // Configure webhook 453 | $subscribeWebhook = [ 454 | 'url' => $webhookurl, 455 | 'events' => [ 456 | 'subscribe' => true, 457 | 'unsubscribe' => true, 458 | 'profile' => true, 459 | 'cleaned' => true, 460 | 'upemail' => true, 461 | 'campaign' => true 462 | ], 463 | 'sources' => [ 464 | 'user' => true, 465 | 'admin' => true, 466 | 'api' => false // to avoid double (infinite loop) update (update an subscriber with the API and the webhook reupdate the user, ...) 467 | ] 468 | ]; 469 | 470 | return $this->addWebhook($listId, $subscribeWebhook); 471 | } 472 | 473 | /** 474 | * Add a new webhook to a list 475 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/webhooks/# 476 | * @param string $listId 477 | * @param array $webhookData 478 | * @return array 479 | */ 480 | public function addWebhook($listId, array $webhookData) 481 | { 482 | $result = $this->mailchimp->post("lists/$listId/webhooks", $webhookData); 483 | 484 | if (!$this->mailchimp->success()) { 485 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 486 | } 487 | 488 | return $result; 489 | } 490 | 491 | /** 492 | * Get webhooks of a list 493 | * http://developer.mailchimp.com/documentation/mailchimp/reference/lists/webhooks/# 494 | * @param string $listId 495 | * @return array 496 | */ 497 | public function getWebhooks($listId) 498 | { 499 | $result = $this->mailchimp->get("lists/$listId/webhooks"); 500 | 501 | if (!$this->mailchimp->success()) { 502 | $this->throwMailchimpError($this->mailchimp->getLastResponse()); 503 | } 504 | 505 | return $result; 506 | } 507 | 508 | /** 509 | * [throwMailchimpError description] 510 | * @param array $errorResponse [description] 511 | * @return void 512 | * @throws MailchimpException [description] 513 | */ 514 | private function throwMailchimpError(array $errorResponse) 515 | { 516 | $errorArray = json_decode($errorResponse['body'], true); 517 | if (is_array($errorArray) && array_key_exists('errors', $errorArray)) { 518 | throw new MailchimpException( 519 | $errorArray['status'], 520 | $errorArray['detail'], 521 | $errorArray['type'], 522 | $errorArray['title'], 523 | $errorArray['errors'], 524 | $errorArray['instance'] 525 | ); 526 | } else { 527 | throw new MailchimpException( 528 | $errorArray['status'], 529 | $errorArray['detail'], 530 | $errorArray['type'], 531 | $errorArray['title'], 532 | null, 533 | $errorArray['instance'] 534 | ); 535 | } 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/Subscriber/ListSynchronizer.php: -------------------------------------------------------------------------------- 1 | listRepository = $listRepository; 21 | } 22 | 23 | /** 24 | * Synchronise user from provider with MailChimp List 25 | * @param SubscriberListInterface $list 26 | * @return void 27 | */ 28 | public function synchronize(SubscriberListInterface $list) 29 | { 30 | $listData = $this->listRepository->findById($list->getListId()); 31 | 32 | // get Subscribers from the provider 33 | $subscribers = $list->getProvider()->getSubscribers(); 34 | 35 | // unsubscribe difference 36 | $this->unsubscribeDifference($list->getListId(), $subscribers); 37 | // subscribe the rest 38 | return $this->batchSubscribe($list->getListId(), $subscribers); 39 | } 40 | 41 | /** 42 | * Subscribe a batch of user 43 | * @param string $listId 44 | * @param array $subscribers 45 | * @return void 46 | */ 47 | protected function batchSubscribe($listId, array $subscribers = []) 48 | { 49 | return $this->listRepository->batchSubscribe($listId, $subscribers); 50 | } 51 | 52 | /** 53 | * Unsubscribe the difference between the array subscriber an user 54 | * @param string $listId 55 | * @param array $subscribers 56 | * @return void 57 | */ 58 | protected function unsubscribeDifference($listId, array $subscribers) 59 | { 60 | $mailchimpEmails = $this->listRepository->getSubscriberEmails($listId); 61 | $internalEmails = array_map(function (Subscriber $subscriber) { 62 | return $subscriber->getEmail(); 63 | }, $subscribers); 64 | 65 | // emails that are present in mailchimp but not internally should be unsubscribed 66 | $diffenceEmails = array_diff($mailchimpEmails, $internalEmails); 67 | if (sizeof($diffenceEmails) == 0) { 68 | return; 69 | } 70 | 71 | $this->listRepository->batchUnsubscribe($listId, $diffenceEmails); 72 | } 73 | 74 | /** 75 | * Synchronize Merge fields of a list and the array $mergeFields 76 | * @param string $listId 77 | * @param array $mergeFields 78 | * @return void 79 | */ 80 | public function synchronizeMergeFields($listId, array $mergeFields = []) 81 | { 82 | $mailChimpMergeFields = $this->listRepository->getMergeFields($listId); 83 | 84 | foreach ($mailChimpMergeFields as $tag) { 85 | if (!$this->tagExists($tag['tag'], $mergeFields)) { 86 | // tag only exist in mailchimp, we are removing it 87 | $this->listRepository->deleteMergeField($listId, $tag['merge_id']); 88 | } 89 | } 90 | 91 | foreach ($mergeFields as $tag) { 92 | if ($tagId = $this->tagExists($tag['tag'], $mailChimpMergeFields)) { 93 | // update mergeField in mailChimp 94 | $this->listRepository->updateMergeField($listId, $tagId, $tag); 95 | } else { 96 | $this->listRepository->addMergeField($listId, $tag); 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * Test if the merge field Tag exists in an array 103 | * @param string $tagName 104 | * @param array $tags 105 | * @return mixed (Boolean true|false) or $tag['merge_id'] 106 | */ 107 | protected function tagExists($tagName, array $tags) 108 | { 109 | foreach ($tags as $tag) { 110 | if ($tag['tag'] == $tagName) { 111 | if (array_key_exists('merge_id', $tag)) { 112 | return $tag['merge_id']; 113 | } 114 | return true; 115 | } 116 | } 117 | return false; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Subscriber/Subscriber.php: -------------------------------------------------------------------------------- 1 | email = $email; 38 | $this->mergeFields = $mergeFields; 39 | $this->options = $options; 40 | } 41 | 42 | /** 43 | * Formate Subscriber for MailChimp API request 44 | * @return array 45 | */ 46 | public function formatMailChimp() 47 | { 48 | $options = $this->options; 49 | if (!empty($this->getMergeFields())) { 50 | $options = array_merge([ 51 | 'merge_fields' => $this->getMergeFields() 52 | ], $options); 53 | } 54 | 55 | return array_merge([ 56 | 'email_address' => $this->getEmail() 57 | ], $options); 58 | } 59 | 60 | /** 61 | * Correspond to email_address in MailChimp request 62 | * @return string 63 | */ 64 | public function getEmail() 65 | { 66 | return $this->email; 67 | } 68 | 69 | /** 70 | * Set mergefields 71 | * @param array $mergeFields 72 | * @return array ['TAGKEY' => value, ...] 73 | */ 74 | public function setMergeFields(array $mergeFields) 75 | { 76 | // since fev2017, MailChimp API doesn't handle null value en throw 400 77 | // errors when you try to add subscriber with a mergefields value to null 78 | foreach ($mergeFields as $key => $value) { 79 | if ($value === null) { 80 | unset($mergeFields[$key]); 81 | } 82 | } 83 | $this->mergeFields = $mergeFields; 84 | 85 | return $this->mergeFields; 86 | } 87 | 88 | /** 89 | * Correspond to merge_fields in MailChimp request 90 | * @return array ['TAGKEY' => value, ...] 91 | */ 92 | public function getMergeFields() 93 | { 94 | // since fev2017, MailChimp API doesn't handle null value en throw 400 95 | // errors when you try to add subscriber with a mergefields value to null 96 | foreach ($this->mergeFields as $key => $value) { 97 | if ($value === null) { 98 | unset($this->mergeFields[$key]); 99 | } 100 | } 101 | return $this->mergeFields; 102 | } 103 | 104 | /** 105 | * The rest of member options: 106 | * email_type, interests, language, vip, location, ip_signup, timestamp_signup, ip_opt, timestamp_opt 107 | * @return array 108 | */ 109 | public function getOptions() 110 | { 111 | return $this->options; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Subscriber/SubscriberList.php: -------------------------------------------------------------------------------- 1 | listId = $listId; 52 | $this->provider = $provider; 53 | $this->mergeFields = $mergeFields; 54 | 55 | //If the provider implements DynamicProviderInterface, set the list id in the provider 56 | if ($this->provider instanceof DynamicProviderInterface) { 57 | $this->provider->setListId($this->listId); 58 | } 59 | } 60 | 61 | /** 62 | * get the MailChimp ListId 63 | * @return string 64 | */ 65 | public function getListId() 66 | { 67 | return $this->listId; 68 | } 69 | 70 | /** 71 | * Get the subscriber provider 72 | * @return ProviderInterface 73 | */ 74 | public function getProvider() 75 | { 76 | return $this->provider; 77 | } 78 | 79 | /** 80 | * Get the list merge fields 81 | * @return array 82 | */ 83 | public function getMergeFields() 84 | { 85 | return $this->mergeFields; 86 | } 87 | 88 | /** 89 | * Get the list webhook URL 90 | * @return string 91 | */ 92 | public function getWebhookUrl() 93 | { 94 | return $this->webhookUrl; 95 | } 96 | 97 | /** 98 | * Set the list webhook URL 99 | * @param string 100 | */ 101 | public function setWebhookUrl($webhookUrl) 102 | { 103 | $this->webhookUrl = $webhookUrl; 104 | } 105 | 106 | /** 107 | * Get the list webhook secret 108 | * @return string 109 | */ 110 | public function getWebhookSecret() 111 | { 112 | return $this->webhookSecret; 113 | } 114 | 115 | /** 116 | * Set the list webhook secret 117 | * @param string 118 | */ 119 | public function setWebhookSecret($webhookSecret) 120 | { 121 | $this->webhookSecret = $webhookSecret; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Subscriber/SubscriberListInterface.php: -------------------------------------------------------------------------------- 1 | add(new SynchronizeSubscribersCommand()); 21 | 22 | $command = $application->find('welp:mailchimp:synchronize-subscribers'); 23 | $commandTester = new CommandTester($command); 24 | $commandTester->execute(array( 25 | 'command' => $command->getName(), 26 | 27 | // pass arguments to the helper 28 | //'username' => 'Wouter', 29 | 30 | // prefix the key with a double slash when passing options, 31 | // e.g: '--some-option' => 'option_value', 32 | )); 33 | 34 | // the output of the command in the console 35 | $output = $commandTester->getDisplay(); 36 | //$this->assertContains('Username: Wouter', $output); 37 | 38 | */ 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Provider/configListProviderTest.php: -------------------------------------------------------------------------------- 1 | providerFactory = $this->createMock(\Welp\MailchimpBundle\Provider\ProviderFactory::class); 18 | 19 | $this->providerFactory 20 | ->method('create') 21 | ->will($this->returnValue($this->createMock(\Welp\MailchimpBundle\Provider\FosSubscriberProvider::class))) 22 | ; 23 | 24 | $this->listConfig = array( 25 | 'sampleid' => array( 26 | 'subscriber_provider' => 'sample.provider', 27 | 'webhook_url' => 'url', 28 | 'webhook_secret' => 'secret', 29 | 'merge_fields' => array( 30 | array( 31 | 'tag' => 'FNAME', 32 | 'name' => 'First Name', 33 | 'type'=> 'text', 34 | 'public' => true 35 | ), 36 | array( 37 | 'tag' => 'LNAME', 38 | 'name' => 'Last Name', 39 | 'type'=> 'text', 40 | 'public' => true 41 | ) 42 | ) 43 | ), 44 | 'sampleid2' => array( 45 | 'subscriber_provider' => 'sample.provider', 46 | 'webhook_url' => 'url', 47 | 'webhook_secret' => 'secret', 48 | 'merge_fields' => array( 49 | array( 50 | 'tag' => 'FNAME2', 51 | 'name' => 'First Name', 52 | 'type'=> 'text', 53 | 'public' => true 54 | ), 55 | array( 56 | 'tag' => 'LNAME2', 57 | 'name' => 'Last Name', 58 | 'type'=> 'text', 59 | 'public' => true 60 | ) 61 | ) 62 | ) 63 | ); 64 | } 65 | 66 | public function testGetListsEmpty() 67 | { 68 | //test with empty lists 69 | $configListProvider = new ConfigListProvider($this->providerFactory, array()); 70 | $this->expectException(\RuntimeException::class); 71 | $configListProvider->getLists(); 72 | } 73 | 74 | public function testGetLists() 75 | { 76 | 77 | $configListProvider = new ConfigListProvider($this->providerFactory, $this->listConfig); 78 | $lists = $configListProvider->getLists(); 79 | 80 | $this->assertEquals(2, count($lists)); 81 | $this->assertTrue($lists[0] instanceof SubscriberList); 82 | $this->assertEquals("FNAME", $lists[0]->getMergeFields()[0]['tag']); 83 | } 84 | 85 | public function testGetList() 86 | { 87 | 88 | $configListProvider = new ConfigListProvider($this->providerFactory, $this->listConfig); 89 | $list = $configListProvider->getList("sampleid2"); 90 | 91 | $this->assertTrue($list instanceof SubscriberList); 92 | $this->assertEquals("sampleid2", $list->getListId()); 93 | $this->assertEquals("FNAME2", $list->getMergeFields()[0]['tag']); 94 | $this->assertEquals("LNAME2", $list->getMergeFields()[01]['tag']); 95 | $this->assertEquals("url", $list->getWebhookUrl()); 96 | $this->assertEquals("secret", $list->getWebhookSecret()); 97 | } 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /tests/Subscriber/ListRepositoryTest.php: -------------------------------------------------------------------------------- 1 | listRepository = new ListRepository($mailchimp); 25 | } 26 | 27 | public function testGetList() 28 | { 29 | $list = $this->listRepository->findById(self::LIST_ID); 30 | //var_dump($list); 31 | $this->assertNotEmpty($list); 32 | $this->assertEquals($list['id'], self::LIST_ID); 33 | } 34 | 35 | public function testSubscribe() 36 | { 37 | // /!\ if toto already exist, this test will throw an error... 38 | $subscriber = new Subscriber('toto@gmail.com', ['FNAME' => 'Toto', 'LNAME' => 'TEST'], ['language' => 'fr']); 39 | $result = $this->listRepository->subscribe(self::LIST_ID, $subscriber); 40 | //var_dump($result); 41 | $this->assertNotEmpty($result); 42 | $this->assertEquals($result['email_address'], 'toto@gmail.com'); 43 | $this->assertEquals($result['status'], 'subscribed'); 44 | } 45 | 46 | public function testUnsubscribe() 47 | { 48 | $subscriber = new Subscriber('toto@gmail.com', ['FNAME' => 'Toto', 'LNAME' => 'TEST'], ['language' => 'fr']); 49 | $result = $this->listRepository->unsubscribe(self::LIST_ID, $subscriber); 50 | //var_dump($result); 51 | $this->assertNotEmpty($result); 52 | $this->assertEquals($result['email_address'], 'toto@gmail.com'); 53 | $this->assertEquals($result['status'], 'unsubscribed'); 54 | } 55 | 56 | public function testDelete() 57 | { 58 | $subscriber = new Subscriber('toto@gmail.com', ['FNAME' => 'Toto', 'LNAME' => 'TEST'], ['language' => 'fr']); 59 | $result = $this->listRepository->delete(self::LIST_ID, $subscriber); 60 | $this->assertTrue($result); 61 | } 62 | 63 | public function testBatchSubscribe() 64 | { 65 | $subscribers = []; 66 | $subscriber1 = new Subscriber('tata@gmail.com', ['FNAME' => 'Tata', 'LNAME' => 'TAST'], ['language' => 'fr']); 67 | array_push($subscribers, $subscriber1); 68 | $subscriber2 = new Subscriber('tete@gmail.com', ['FNAME' => 'Tete', 'LNAME' => 'TEEST'], ['language' => 'fr']); 69 | array_push($subscribers, $subscriber2); 70 | $subscriber3 = new Subscriber('tztz@gmail.com', ['FNAME' => 'Tztz', 'LNAME' => 'TZST'], ['language' => 'fr']); 71 | array_push($subscribers, $subscriber3); 72 | $subscriber4 = new Subscriber('trtr@gmail.com', ['FNAME' => 'Trtr', 'LNAME' => 'TRST'], ['language' => 'fr']); 73 | array_push($subscribers, $subscriber4); 74 | 75 | $result = $this->listRepository->batchSubscribe(self::LIST_ID, $subscribers); 76 | 77 | //var_dump($result); 78 | $this->assertNotEmpty($result); 79 | } 80 | 81 | public function testBatchUnsubscribe() 82 | { 83 | $emails = [ 84 | 'tata@gmail.com', 85 | 'tete@gmail.com', 86 | 'tztz@gmail.com', 87 | ]; 88 | 89 | $result = $this->listRepository->batchUnsubscribe(self::LIST_ID, $emails); 90 | 91 | //var_dump($result); 92 | $this->assertNotEmpty($result); 93 | } 94 | 95 | public function testBatchDelete() 96 | { 97 | $emails = [ 98 | 'tata@gmail.com', 99 | 'tete@gmail.com', 100 | 'tztz@gmail.com', 101 | 'trtr@gmail.com', 102 | ]; 103 | 104 | $result = $this->listRepository->batchDelete(self::LIST_ID, $emails); 105 | 106 | //var_dump($result); 107 | $this->assertNotEmpty($result); 108 | } 109 | 110 | public function testChangeEmail() 111 | { 112 | // No solution so far with the API V3 113 | // Waiting for further informations 114 | /// http://stackoverflow.com/questions/32224697/mailchimp-api-v3-0-change-subscriber-email 115 | /*$subscriber = new Subscriber('toto@gmail.com', ['FNAME' => 'Toto', 'LNAME' => 'TEST'], ['language' => 'fr']); 116 | $this->listRepository->subscribe(self::LIST_ID, $subscriber); 117 | 118 | $result = $this->listRepository->changeEmailAddress(self::LIST_ID, $subscriber->getEmail(), 'ratata@gmail.com'); 119 | var_dump($result); 120 | $this->assertNotEmpty($result); 121 | */ 122 | } 123 | 124 | public function testMergeTags() 125 | { 126 | $result = $this->listRepository->addMergeField(self::LIST_ID, ['name' => 'Test', 'type' => 'text', 'tag' => 'MYTEST']); 127 | //var_dump($result); 128 | $this->assertNotEmpty($result); 129 | $this->assertEquals($result['tag'], 'MYTEST'); 130 | $result2 = $this->listRepository->updateMergeField(self::LIST_ID, $result['merge_id'], ['tag' => 'SECONDTEST']); 131 | //var_dump($result2); 132 | $this->assertNotEmpty($result2); 133 | $this->assertEquals($result2['tag'], 'SECONDTEST'); 134 | $result3 = $this->listRepository->deleteMergeField(self::LIST_ID, $result2['merge_id']); 135 | $this->assertTrue($result3); 136 | } 137 | 138 | public function testWebhook() 139 | { 140 | //$result = $this->listRepository->registerMainWebhook(self::LIST_ID, 'http://requestb.in/rkhf26rk'); 141 | 142 | //var_dump($result); 143 | //$this->assertNotEmpty($result); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/Subscriber/ListSynchronizerTest.php: -------------------------------------------------------------------------------- 1 | listRepository = new ListRepository($mailchimp); 24 | } 25 | 26 | public function testSynchronize() 27 | { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Subscriber/SubscriberTest.php: -------------------------------------------------------------------------------- 1 | getSubscriber($email, $fields, $options)->formatMailChimp(); 24 | 25 | static::assertEquals($expected, $result); 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function provider() 32 | { 33 | return [ 34 | [ 35 | 'test@gmail.com', 36 | ['field1' => 'field1', 'nullable' => null], 37 | ['option' => 'option_value'], 38 | //expected merge_fields 39 | ['merge_fields' => ['field1' => 'field1'], 'option' => 'option_value', 'email_address' => 'test@gmail.com'], 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * @param $email 46 | * @param $fields 47 | * @param $options 48 | * 49 | * @return Subscriber 50 | */ 51 | private function getSubscriber($email, $fields, $options) 52 | { 53 | return new Subscriber($email, $fields, $options); 54 | } 55 | } 56 | --------------------------------------------------------------------------------