diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing-0.2.png b/doc/fr/Sil/Component/Emailing/domain/emailing-0.2.png new file mode 100644 index 00000000..b246cab3 Binary files /dev/null and b/doc/fr/Sil/Component/Emailing/domain/emailing-0.2.png differ diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing.uxf b/doc/fr/Sil/Component/Emailing/domain/emailing.uxf new file mode 100644 index 00000000..1e503515 --- /dev/null +++ b/doc/fr/Sil/Component/Emailing/domain/emailing.uxf @@ -0,0 +1,458 @@ + + + 10 + + UMLFrame + + 70 + 20 + 1210 + 650 + + EmailingComponent + + + + UMLClass + + 580 + 150 + 160 + 60 + + /AbstractMessage/ +-- +#title :string +#content: string +-- + + + + UMLClass + + 460 + 340 + 160 + 50 + + template=Resource +SimpleMessage +-- + + + + + UMLClass + + 720 + 340 + 160 + 50 + + template=Resource +GroupedMessage +-- + + + + UMLClass + + 560 + 430 + 200 + 90 + + template=Resource +MailingList +-- +#name: string +#description: string +#enabled: bool + + + + UMLClass + + 560 + 590 + 200 + 60 + + template=Resource +Recipient +-- +#valid: bool + + + + UMLClass + + 580 + 70 + 160 + 40 + + <<Interface>> +MessageInterface +-- + + + + + UMLClass + + 90 + 70 + 180 + 40 + + <<Interface>> +AttachmentInterface +-- + + + + + Relation + + 260 + 70 + 340 + 120 + + lt=- +r1=0..1 +m1=#message +r2=0..n +m2=#attachments + 320.0;90.0;130.0;90.0;130.0;20.0;10.0;20.0 + + + Relation + + 650 + 100 + 30 + 70 + + lt=<<. + 10.0;10.0;10.0;50.0 + + + Relation + + 510 + 200 + 170 + 170 + + lt=<<- + 150.0;10.0;150.0;70.0;10.0;70.0;10.0;150.0 + + + Relation + + 650 + 260 + 150 + 110 + + lt=- + 10.0;10.0;130.0;10.0;130.0;90.0 + + + Relation + + 710 + 360 + 200 + 140 + + lt=- +r1=0..n +m1=#lists + 10.0;110.0;180.0;110.0;180.0;10.0;130.0;10.0 + + + Relation + + 710 + 490 + 200 + 170 + + lt=- +r1=1..n +m1=#lists +r2=1..n +m2=#recipients + 10.0;20.0;180.0;20.0;180.0;140.0;10.0;140.0 + + + UMLClass + + 90 + 330 + 190 + 220 + + template=Resource +EmailAddress +-- +-value: string + + + + Relation + + 230 + 510 + 350 + 140 + + lt=- +m2=email +r2=1..1 + 330.0;120.0;100.0;120.0;100.0;20.0;10.0;20.0 + + + Relation + + 230 + 360 + 250 + 160 + + lt=- +m2=#from +r2=1..1 + 230.0;10.0;100.0;10.0;100.0;130.0;10.0;130.0 + + + Relation + + 230 + 430 + 120 + 50 + + lt=- +m2=#to +r2=1..n + 100.0;20.0;10.0;20.0 + + + Relation + + 230 + 390 + 120 + 50 + + lt=- +m2=#cc +r2=0..n + 100.0;20.0;10.0;20.0 + + + Relation + + 230 + 350 + 120 + 50 + + lt=- +m2=#bcc +r2=0..n + 100.0;20.0;10.0;20.0 + + + UMLClass + + 970 + 50 + 190 + 70 + + template=Resource +MessageTemplate +-- +#name: string +#content: string + + + + + UMLClass + + 970 + 290 + 190 + 70 + + template=Resource +ContentToken +-- +#value: mixed + + + + + Relation + + 1110 + 60 + 170 + 150 + + lt=- +r1=0..n +m1=#tokenTypes +r2=1..1 +m2=#template + 10.0;120.0;150.0;120.0;150.0;20.0;10.0;20.0 + + + Relation + + 730 + 60 + 260 + 130 + + lt=- +r2=0..n +m2=#messages +r1=0..1 +m1=#template + 240.0;20.0;140.0;20.0;140.0;100.0;10.0;100.0 + + + Relation + + 730 + 180 + 260 + 160 + + lt=- +r2=1..1 +m2=#message +r1=0..n +m1=#tokens + 240.0;130.0;140.0;130.0;140.0;20.0;10.0;20.0 + + + UMLClass + + 970 + 140 + 190 + 110 + + template=Resource +ContentTokenType +-- +#name: string + + + + + Relation + + 1110 + 220 + 120 + 120 + + lt=- +r2=1..1 +m2=#type + 10.0;100.0;100.0;100.0;100.0;20.0;10.0;20.0 + + + UMLClass + + 960 + 420 + 170 + 130 + + <<enum>> +ContentTokenDataType +-- +BOOLEAN +STRING +INTEGER +FLOAT +DATE +DATETIME + + + + + Relation + + 1110 + 200 + 170 + 270 + + lt=- +r1=1..1 +m1=#dataType + 20.0;240.0;150.0;240.0;150.0;10.0;10.0;10.0 + + + UMLClass + + 90 + 180 + 180 + 40 + + <<Interface>> +MessageStateInterface +-- + + + + + UMLClass + + 90 + 250 + 180 + 30 + + MessageState +-- + + + + + Relation + + 170 + 210 + 30 + 60 + + lt=<<. + 10.0;10.0;10.0;40.0 + + + Relation + + 260 + 180 + 340 + 50 + + lt=- +r2=1..1 +m2=#state + 320.0;20.0;10.0;20.0 + + diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing_message_states-0.1.png b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states-0.1.png new file mode 100644 index 00000000..e81a0640 Binary files /dev/null and b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states-0.1.png differ diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing_message_states.uxf b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states.uxf new file mode 100644 index 00000000..01c52e41 --- /dev/null +++ b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states.uxf @@ -0,0 +1,175 @@ + + + 10 + + UMLSpecialState + + 650 + 210 + 20 + 20 + + type=initial + + + + Relation + + 650 + 220 + 70 + 90 + + lt=-> +create + 10.0;10.0;10.0;70.0 + + + UMLState + + 610 + 290 + 100 + 40 + + DRAFT + + + + UMLState + + 610 + 410 + 100 + 40 + + VALIDATED + + + + Relation + + 650 + 320 + 80 + 110 + + lt=-> +validate + 10.0;10.0;10.0;90.0 + + + UMLState + + 610 + 530 + 100 + 40 + + SENT + + + + Relation + + 650 + 440 + 60 + 110 + + lt=-> +send + 10.0;10.0;10.0;90.0 + + + Relation + + 700 + 410 + 150 + 40 + + lt=- +delete + 10.0;20.0;130.0;20.0 + + + Relation + + 700 + 290 + 150 + 260 + + lt=-> +delete + 10.0;20.0;130.0;20.0;130.0;240.0 + + + UMLSpecialState + + 650 + 630 + 20 + 20 + + type=final + + + + UMLFrame + + 460 + 170 + 460 + 490 + + Mailing State Machine + + + + UMLState + + 780 + 530 + 100 + 40 + + DELETED + + + + Relation + + 650 + 560 + 200 + 60 + + lt=- + 180.0;10.0;180.0;40.0;10.0;40.0 + + + Relation + + 650 + 560 + 30 + 90 + + lt=-> + 10.0;10.0;10.0;70.0 + + + Relation + + 540 + 490 + 110 + 80 + + lt=-> +re-send + 70.0;60.0;10.0;60.0;10.0;10.0;90.0;10.0;90.0;40.0 + + diff --git a/doc/fr/Sil/Component/Emailing/domain/index.rst b/doc/fr/Sil/Component/Emailing/domain/index.rst new file mode 100644 index 00000000..44764d3a --- /dev/null +++ b/doc/fr/Sil/Component/Emailing/domain/index.rst @@ -0,0 +1,214 @@ +======== +Emailing +======== + +---------------------- +Description du domaine +---------------------- + +L'**emailing** consiste à **préparer** des envois d'emails en masse. + +Cela consiste à créer une contenu textuel riche (HTML) et le diffuser à une liste de destinataires appelée **liste de diffusion**. + +L'utilisation du mailing se fait principalement dans un cadre marketing, pour informer sa clientèle ou sa communauté, réaliser des prospections et démarchages, ou communiquer sur des actions commerciales promotionnelles. + +Il peut également servir pour des envois ponctuels (sans passer par des listes de diffusions). + +---------------------- +Fonctionnalités cibles +---------------------- + +- `Gestion du contenu`_ +- `Gestion des listes de diffusion`_ +- `Paramétrage des envois`_ +- `Gestion d'envoi simple`_ +- `Gestion de modèles`_ + +Gestion du contenu +================== + +Saisir le titre du message, son contenu (via éditeur de texte riche (éditeur WYSIWYG)), ses pièces jointes. + +Il faut également gérer son cycle de vie. Selon son état, un mailing ne pourra être envoyé, modifié ou archivé. + +Gestion des listes de diffusion +=============================== + +Le composant doit pouvoir proposer une gestion de liste de diffusion. une liste de diffusion est nommée et est composée d'un ou plusieurs destinataires. + +Paramétrage des envois +====================== + +Pour chaque message simple, il doit être possible de paramétrer des informations liées à l'envoi : expéditeur, répondre à, copie. + +Gestion d'envoi simple +====================== + +Pouvoir gérer des envois de mail simples : A destination d'une seul contact. Il faut également conserver l'historique des envois par contacts. + +Gestion de modèles +================== + +Une gestion de modèle de contenu doit permettre la création rapide de campagne d'emailing. Il faudra également prévoir un système de remplacement de jeton [1]_ pour permettre de pré-remplir certaines informations. + +.. note:: + + .. [1] Un jeton est un emplacement dans un contenu texte qui sera substitué par une valeur lors de la construction du contenu + +------- +Domaine +------- + +Message groupé +============== + +Un **message groupé** est définit de cette manière : + ++-------------+---------------------------------------------------------+--------+ +| Propriété | Description | Oblig. | ++=============+=========================================================+========+ +| title | Le titre du message. Sera également l'objet de l'email. | x | ++-------------+---------------------------------------------------------+--------+ +| content | Le contenu du message. | | ++-------------+---------------------------------------------------------+--------+ +| attachments | Les pièces jointes du message. | | ++-------------+---------------------------------------------------------+--------+ +| lists | Les listes de diffusion qui seront utilisées. | | ++-------------+---------------------------------------------------------+--------+ + +Message simple +============== + +Un **message simple** est définit de cette manière : + ++-------------+---------------------------------------------------------+--------+ +| Propriété | Description | Oblig. | ++=============+=========================================================+========+ +| title | Le titre du message. Sera également l'objet de l'email. | x | ++-------------+---------------------------------------------------------+--------+ +| content | Le contenu du message. | | ++-------------+---------------------------------------------------------+--------+ +| attachments | Les pièces jointes du message. | | ++-------------+---------------------------------------------------------+--------+ +| config | La configuration d'expédition du message. | | ++-------------+---------------------------------------------------------+--------+ + +Configuration de message +======================== + +Une **configuration de message** gère les paramètres suivants : + ++-----------+-----------------------------------------------+ +| Propriété | Description | ++===========+===============================================+ +| from | L'expéditeur qui sera définit pour le message | ++-----------+-----------------------------------------------+ +| to | Le destinataire du message [2]_ | ++-----------+-----------------------------------------------+ +| cc | Une adresse email qui sera mise en copie [2]_ | ++-----------+-----------------------------------------------+ +| bcc | Une adresse email en copie cachée [2]_ | ++-----------+-----------------------------------------------+ + +.. note:: + + .. [2] Ce paramètre du message sera utilisé que lors d'envoi de message simple (hors listes de diffusion) + +Liste de diffusion +================== + +Une **liste de diffusion** se définit par un titre et une collection de **destinataires**. + ++-------------+--------------------------------+--------+ +| Propriété | Description | Oblig. | ++=============+================================+========+ +| title | Le titre de la liste | x | ++-------------+--------------------------------+--------+ +| description | Une description optionnelle | | ++-------------+--------------------------------+--------+ +| enabled | La list est utilisable ou non | | ++-------------+--------------------------------+--------+ +| recipients | Une collection de destinataire | | ++-------------+--------------------------------+--------+ + +Destinataire +============ + +Un **destinataire** est une représentation d'une adresse email. + ++-----------+----------------------------------------------------+--------+ +| Propriété | Description | Oblig. | ++===========+====================================================+========+ +| email | L'adresse email du destinataire | x | ++-----------+----------------------------------------------------+--------+ +| valid | Un indicateur d'état de validité de l'adresse [3]_ | | ++-----------+----------------------------------------------------+--------+ + +.. note:: + + .. [3] Cet indicateur sera à mettre à jour en fonction des retours après envoi. (voir https://en.wikipedia.org/wiki/Bounce_message) + +Pièce jointe +============ + +Une pièce jointe représentera un fichier à joindre au message. + +Modèle de message +================= + +Un **modèle de message** permet de définir une mise en page de base pour les messages ainsi que la définition de jetons de substitution pour faciliter la saisie des messages utilisant un modèle. + ++-----------+-----------------------------------------------+--------+ +| Propriété | Description | Oblig. | ++===========+===============================================+========+ +| content | Contenu du modèle | | ++-----------+-----------------------------------------------+--------+ +| tokens | Collection de types de jetons de substitution | | ++-----------+-----------------------------------------------+--------+ + +Type de jeton de substitution +============================= + +Un **type de jeton** permet de définir quelle donnée sera affichée dans un modèle de message. + ++-----------+-----------------------+--------+ +| Propriété | Description | Oblig. | ++===========+=======================+========+ +| name | Nom du type de donnée | | ++-----------+-----------------------+--------+ +| dataType | Type de donnée cible | | ++-----------+-----------------------+--------+ + +Jeton de substitution +===================== + +Un **jeton de substitution** permet de remplacer des emplacements définis depuis un modèle par des valeurs de substitution. + ++-----------+---------------------------------+--------+ +| Propriété | Description | Oblig. | ++===========+=================================+========+ +| value | Valeur du jeton de substitution | | ++-----------+---------------------------------+--------+ +| type | Type de jeton | x | ++-----------+---------------------------------+--------+ + +----------------- +Modèle du domaine +----------------- + +.. image:: emailing-0.2.png + +------------ +Cycle de vie +------------ + +Message +======= + +Un message, qu'il soit simple ou groupé, suivra le même cycle de vie suivant : + +.. image:: emailing_message_states-0.1.png + +.. note:: + + Un message envoyé pourra être envoyé à nouveau, ceci sans limite. Il sera à la charge des implémentations de limiter ou non cette particularité. diff --git a/doc/fr/Sil/Component/Emailing/index.rst b/doc/fr/Sil/Component/Emailing/index.rst new file mode 100644 index 00000000..1ec1d93c --- /dev/null +++ b/doc/fr/Sil/Component/Emailing/index.rst @@ -0,0 +1,8 @@ +Composants Emailing +=================== + +.. toctree:: + :maxdepth: 2 + + installation + domain/index diff --git a/doc/fr/Sil/Component/Emailing/installation.rst b/doc/fr/Sil/Component/Emailing/installation.rst new file mode 100644 index 00000000..11e44375 --- /dev/null +++ b/doc/fr/Sil/Component/Emailing/installation.rst @@ -0,0 +1,2 @@ +Installation +============ diff --git a/doc/fr/Sil/Component/map.rst.inc b/doc/fr/Sil/Component/map.rst.inc index ec3f23a8..a8a8376a 100644 --- a/doc/fr/Sil/Component/map.rst.inc +++ b/doc/fr/Sil/Component/map.rst.inc @@ -8,4 +8,5 @@ * :doc:`/Sil/Component/Product/index` * :doc:`/Sil/Component/Stock/index` * :doc:`/Sil/Component/Uom/index` +* :doc:`/Sil/Component/Emailing/index` * :doc:`/Sil/Component/User/index` diff --git a/doc/fr/conf.py b/doc/fr/conf.py index a99b1797..dbba8534 100644 --- a/doc/fr/conf.py +++ b/doc/fr/conf.py @@ -20,7 +20,7 @@ master_doc = 'index' project = u'Sil & Blast Projects' -copyright = u'2017, Libre-Informatique' +copyright = u'2018, Libre-Informatique' version = '' release = '' diff --git a/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php b/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php index f7af66ff..6ab740bf 100644 --- a/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php +++ b/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php @@ -1,7 +1,7 @@ shuf.nbr +fi + +#RND contain branch name which may contain '-' which may not work as database name for postgre + +RND=$(echo $RND|sed -e s/-/_/g|tr '[:upper:]' '[:lower:]')$(echo -n $(cat shuf.nbr )) + +SILURL="/sil" +SERVERADDR="127.0.0.1:8042" +SERVERENV="test" + +DBHOST=postgres.host +DBROOTUSER=postgres +DBROOTPASSWORD=postgres24 +DBAPPNAME=sil_db_${RND} +DBAPPUSER=sil_user_${RND} +DBAPPPASSWORD=sil_password + +ELHOST=elk.host +ELALIAS=sil_${RND} + +PHPUNITCMD="bin/phpunit --verbose --debug -c phpunit.xml.dist --coverage-html build/coverage --coverage-clover build/clover.xml --coverage-crap4j build/crap4j.xml --log-junit build/junit.xml" #--testdox +#CODECEPTCMD="bin/codecept run -vvv --debug --steps --fail-fast --no-interaction --xml --html" +CODECEPTENV="firefox" #chrome firefox,lisem ... +CODECEPTOUTPUT="src/Tests/_output/" + + +DISPLAY=:99 diff --git a/src/Sil/Component/Emailing/.env.travis b/src/Sil/Component/Emailing/.env.travis new file mode 100644 index 00000000..0b042b50 --- /dev/null +++ b/src/Sil/Component/Emailing/.env.travis @@ -0,0 +1,23 @@ + + +SILURL="/sil" +SERVERADDR="127.0.0.1:8042" +SERVERENV="test" + +DBHOST=localhost +DBROOTUSER=postgres +DBROOTPASSWORD=postgres24 +DBAPPNAME=test_sil_db +DBAPPUSER=test_sil_user +DBAPPPASSWORD=test_sil_password + +ELHOST=localhost +ELALIAS=Sil + +PHPUNITCMD="bin/phpunit --verbose --debug -c phpunit.xml.dist --coverage-clover build/logs/clover.xml" #--testdox +#CODECEPTCMD="bin/codecept run -vvv --debug --steps --fail-fast --no-interaction --xml --html" +CODECEPTENV="firefox" #chrome firefox,lisem ... +CODECEPTOUTPUT="src/Tests/_output/" + + +DISPLAY=:99 diff --git a/src/Sil/Component/Emailing/.github/ISSUE_TEMPLATE.md b/src/Sil/Component/Emailing/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..7209c517 --- /dev/null +++ b/src/Sil/Component/Emailing/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +| Q | A +| ---------------- | ----- +| Bug report? | yes/no +| Feature request? | yes/no +| BC Break report? | yes/no +| Version | x.y.z + + diff --git a/src/Sil/Component/Emailing/.github/PULL_REQUEST_TEMPLATE.md b/src/Sil/Component/Emailing/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..b92f1610 --- /dev/null +++ b/src/Sil/Component/Emailing/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +| Q | A +| ------------- | --- +| Branch? | master +| Bug fix? | yes/no +| New feature? | yes/no +| BC breaks? | yes/no +| Tests pass? | yes/no +| Fixed tickets | fixes #X, partially #Y, mentioned in #Z +| License | LGPL + + + diff --git a/src/Sil/Component/Emailing/.gitignore b/src/Sil/Component/Emailing/.gitignore new file mode 100644 index 00000000..6be7c114 --- /dev/null +++ b/src/Sil/Component/Emailing/.gitignore @@ -0,0 +1,11 @@ +/nbproject +/.php_cs.cache +/composer.phar +/composer.lock +/vendor/ +/Resources/doc/_build +*~ +/bin/* +!/bin/console +!/bin/ci-scripts +!/bin/git-script \ No newline at end of file diff --git a/src/Sil/Component/Emailing/.gitrepo b/src/Sil/Component/Emailing/.gitrepo new file mode 100644 index 00000000..98603c72 --- /dev/null +++ b/src/Sil/Component/Emailing/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme +; +[subrepo] + remote = git@github.com:sil-project/Order.git + branch = wip-platform + commit = 4e6c89333ba3b70c866553de3777a438fd8401a0 + method = merge + cmdver = 0.4.0 + parent = 50e93e7427a5053c19cebb6beb86860b5e318920 diff --git a/src/Sil/Component/Emailing/.php_cs b/src/Sil/Component/Emailing/.php_cs new file mode 100644 index 00000000..e41052d3 --- /dev/null +++ b/src/Sil/Component/Emailing/.php_cs @@ -0,0 +1,47 @@ +in(__DIR__) +; + +$config = PhpCsFixer\Config::create() + ->setRules(array( + '@Symfony' => true, + 'binary_operator_spaces' => ['align_double_arrow' => true], + 'concat_space' => ['spacing'=>'one'], + 'yoda_style' => null, + 'increment_style' => ['style' => 'post'], + )) + ->setFinder($finder); + +// PHP-CS-Fixer 2.x +if (method_exists($config, 'setRules')) { + $config->setRules(array_merge($config->getRules(), array( + 'header_comment' => array('header' => $header), + ))); +} + +return $config; diff --git a/src/Sil/Component/Emailing/.scrutinizer.yml b/src/Sil/Component/Emailing/.scrutinizer.yml new file mode 100644 index 00000000..08151b1a --- /dev/null +++ b/src/Sil/Component/Emailing/.scrutinizer.yml @@ -0,0 +1,16 @@ +filter: + excluded_paths: + - 'vendor/*' + - 'bin/*' + - '*.min.js' + - 'Tests/*' +checks: + php: + code_rating: true + duplication: true + javascript: true +coding_style: + php: + spaces: + around_operators: + concatenation: true diff --git a/src/Sil/Component/Emailing/.styleci.yml b/src/Sil/Component/Emailing/.styleci.yml new file mode 100644 index 00000000..977c5a87 --- /dev/null +++ b/src/Sil/Component/Emailing/.styleci.yml @@ -0,0 +1,11 @@ +# Package `sllh/php-cs-fixer-styleci-bridge` is required to get it working. + +preset: psr2 + +#disabled: +# - concat_without_spaces +# - single_quote + +enabled: + - no_php4_constructor + - concat_with_spaces diff --git a/src/Sil/Component/Emailing/.travis.yml b/src/Sil/Component/Emailing/.travis.yml new file mode 100644 index 00000000..bd1d3462 --- /dev/null +++ b/src/Sil/Component/Emailing/.travis.yml @@ -0,0 +1,54 @@ +language: php + +php: + - '7.1' + - nightly + +services: + - mysql + - postgresql + +cache: + directories: + - $HOME/.composer/cache/files + +env: + global: + - PATH="$HOME/.local/bin:$PATH" + - SCRIPTS_FOLDER=bin/ci-scripts + - SYMFONY_DEPRECATIONS_HELPER=weak + - TARGET=test + - XMLLINT_INDENT=" " + +matrix: + fast_finish: true + include: + - php: '7.1' + env: TARGET=docs + - php: '7.1' + env: TARGET=lint + allow_failures: + - php: nightly + - env: SYMFONY_DEPRECATIONS_HELPER=0 + - env: SYMFONY=dev-master@dev + - env: SONATA_CORE=dev-master@dev + - env: SONATA_BLOCK=dev-master@dev + +before_install: + - if [ -x ${SCRIPTS_FOLDER}/before_install_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/before_install_${TARGET}.sh; fi; + - if [ -x ${SCRIPTS_FOLDER}/create_database_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/create_database_${TARGET}.sh; fi; + +install: + - if [ -x ${SCRIPTS_FOLDER}/install_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/install_${TARGET}.sh; fi; + +before_script: + - if [ -x ${SCRIPTS_FOLDER}/before_script_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/before_script_${TARGET}.sh; fi; + +script: + - if [ -x ${SCRIPTS_FOLDER}/run_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/run_${TARGET}.sh; fi; + +after_success: + - if [ -x ${SCRIPTS_FOLDER}/after_success_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/after_success_${TARGET}.sh; fi; + +after_failure: + - if [ -x ${SCRIPTS_FOLDER}/after_failure_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/after_failure_${TARGET}.sh; fi; diff --git a/src/Sil/Component/Emailing/LICENCE.md b/src/Sil/Component/Emailing/LICENCE.md new file mode 100644 index 00000000..408c98d4 --- /dev/null +++ b/src/Sil/Component/Emailing/LICENCE.md @@ -0,0 +1,157 @@ +### GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the +terms and conditions of version 3 of the GNU General Public License, +supplemented by the additional permissions listed below. + +#### 0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the +GNU General Public License. + +"The Library" refers to a covered work governed by this License, other +than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + +The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + +#### 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + +#### 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + +- a) under this License, provided that you make a good faith effort + to ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or +- b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + +#### 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a +header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + +- a) Give prominent notice with each copy of the object code that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the object code with a copy of the GNU GPL and this + license document. + +#### 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken +together, effectively do not restrict modification of the portions of +the Library contained in the Combined Work and reverse engineering for +debugging such modifications, if you also do each of the following: + +- a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the Combined Work with a copy of the GNU GPL and this + license document. +- c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. +- d) Do one of the following: + - 0) Convey the Minimal Corresponding Source under the terms of + this License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + - 1) Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (a) uses at run + time a copy of the Library already present on the user's + computer system, and (b) will operate properly with a modified + version of the Library that is interface-compatible with the + Linked Version. +- e) Provide Installation Information, but only if you would + otherwise be required to provide such information under section 6 + of the GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the Application + with a modified version of the Linked Version. (If you use option + 4d0, the Installation Information must accompany the Minimal + Corresponding Source and Corresponding Application Code. If you + use option 4d1, you must provide the Installation Information in + the manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.) + +#### 5. Combined Libraries. + +You may place library facilities that are a work based on the Library +side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + +- a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities, conveyed under the terms of this License. +- b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + +#### 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +as you received it specifies that a certain numbered version of the +GNU Lesser General Public License "or any later version" applies to +it, you have the option of following the terms and conditions either +of that published version or of any later version published by the +Free Software Foundation. If the Library as you received it does not +specify a version number of the GNU Lesser General Public License, you +may choose any version of the GNU Lesser General Public License ever +published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/src/Sil/Component/Emailing/Model/AbstractMessage.php b/src/Sil/Component/Emailing/Model/AbstractMessage.php new file mode 100644 index 00000000..fa073124 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/AbstractMessage.php @@ -0,0 +1,191 @@ +title = $title; + $this->content = trim($content); + + $this->state = MessageState::draft(); + + $this->attachments = new ArrayCollection(); + $this->tokens = new ArrayCollection(); + } + + /** + * {@inheritdoc} + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * {@inheritdoc} + */ + public function setTitle(string $title): void + { + $this->title = $title; + } + + /** + * {@inheritdoc} + */ + public function getContent(): string + { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent(string $content): void + { + $this->content = trim($content); + } + + /** + * {@inheritdoc} + */ + public function getAttachments(): array + { + return $this->attachments->getValues(); + } + + /** + * {@inheritdoc} + */ + public function addAttachment(AttachmentInterface $attachment): void + { + if ($this->attachments->contains($attachment)) { + throw new InvalidArgumentException(sprintf('Attachment « %s » is already attached to message « %s »', $attachment->getName(), $this->getTitle())); + } + $this->attachments->add($attachment); + } + + /** + * {@inheritdoc} + */ + public function removeAttachment(AttachmentInterface $attachment): void + { + if (!$this->attachments->contains($attachment)) { + throw new InvalidArgumentException(sprintf('Attachment « %s » is not attached to message « %s »', $attachment->getName(), $this->getTitle())); + } + $this->attachments->removeElement($attachment); + } + + /** + * {@inheritdoc} + */ + public function getTemplate(): ?MessageTemplateInterface + { + return $this->template; + } + + /** + * {@inheritdoc} + */ + public function setTemplate(?MessageTemplateInterface $template): void + { + $this->template = $template; + } + + /** + * {@inheritdoc} + */ + public function getTokens(): array + { + return $this->tokens->getValues(); + } + + /** + * {@inheritdoc} + */ + public function addToken(ContentTokenInterface $token): void + { + if ($this->tokens->contains($token)) { + throw new InvalidArgumentException(sprintf('Token « %s : %s » is already assigned to the message « %s »', $token->getName(), $token->getValue(), $this->getTitle())); + } + $this->tokens->add($token); + } + + /** + * {@inheritdoc} + */ + public function removeToken(ContentTokenInterface $token): void + { + if (!$this->tokens->contains($token)) { + throw new InvalidArgumentException(sprintf('Token « %s : %s » is not assigned to the message « %s »', $token->getName(), $token->getValue(), $this->getTitle())); + } + $this->tokens->removeElement($token); + } + + /** + * {@inheritdoc} + */ + public function clearTokens(): void + { + $this->tokens->clear(); + } +} diff --git a/src/Sil/Component/Emailing/Model/AttachmentInterface.php b/src/Sil/Component/Emailing/Model/AttachmentInterface.php new file mode 100644 index 00000000..4851fd5b --- /dev/null +++ b/src/Sil/Component/Emailing/Model/AttachmentInterface.php @@ -0,0 +1,17 @@ +tokenType = $tokenType; + $this->message = $message; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->getTokenType()->getName(); + } + + /** + * {@inheritdoc} + */ + public function getMessage(): MessageInterface + { + return $this->message; + } + + /** + * {@inheritdoc} + */ + public function getValue() + { + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function setValue($value): void + { + $typeIsValid = false; + + switch ($this->getTokenType()->getDataType()->getValue()) { + case ContentTokenDataType::TYPE_BOOLEAN: + $typeIsValid = is_bool($value); + break; + + case ContentTokenDataType::TYPE_DATE: + case ContentTokenDataType::TYPE_DATETIME: + $typeIsValid = get_class($value) === 'DateTime'; + break; + + case ContentTokenDataType::TYPE_STRING: + $typeIsValid = is_string($value); + break; + + case ContentTokenDataType::TYPE_INTEGER: + $typeIsValid = is_int($value); + break; + + case ContentTokenDataType::TYPE_FLOAT: + $typeIsValid = is_float($value); + break; + + default: + throw new InvalidArgumentException(sprintf('The type of the value (%s) of ContentToken « %s » is not managed', $this->getName(), gettype($value))); + } + + if (!$typeIsValid) { + throw new InvalidArgumentException(sprintf('Value of ContentToken « %s » must be of type « %s », « %s » given', $this->getName(), $this->getTokenType()->getDataType(), gettype($value))); + } + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getTokenType(): ContentTokenTypeInterface + { + return $this->tokenType; + } + + /** + * {@inheritdoc} + */ + public function getValueAsString(): string + { + $stringValue = ''; + + switch ($this->getTokenType()->getDataType()->getValue()) { + case ContentTokenDataType::TYPE_BOOLEAN: + $stringValue = $this->value === true ? 'Yes' : 'No'; + break; + + case ContentTokenDataType::TYPE_DATE: + $stringValue = $this->value->format('Y-m-d'); + break; + + case ContentTokenDataType::TYPE_DATETIME: + $stringValue = $this->value->format('Y-m-d H:i:s'); + break; + + case ContentTokenDataType::TYPE_STRING: + case ContentTokenDataType::TYPE_INTEGER: + case ContentTokenDataType::TYPE_FLOAT: + default: + $stringValue = (string) $this->value; + } + + return $stringValue; + } +} diff --git a/src/Sil/Component/Emailing/Model/ContentTokenDataType.php b/src/Sil/Component/Emailing/Model/ContentTokenDataType.php new file mode 100644 index 00000000..60c1bc10 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/ContentTokenDataType.php @@ -0,0 +1,60 @@ +getTypes())) { + throw new InvalidArgumentException(sprintf('Type « %s » is not managed. Managed types are : %s', $value, implode(', ', $this->getTypes()))); + } + $this->value = $value; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + public function getTypes(): array + { + return [ + 'TYPE_BOOLEAN' => static::TYPE_BOOLEAN, + 'TYPE_STRING' => static::TYPE_STRING, + 'TYPE_INTEGER' => static::TYPE_INTEGER, + 'TYPE_FLOAT' => static::TYPE_FLOAT, + 'TYPE_DATE' => static::TYPE_DATE, + 'TYPE_DATETIME' => static::TYPE_DATETIME, + ]; + } +} diff --git a/src/Sil/Component/Emailing/Model/ContentTokenInterface.php b/src/Sil/Component/Emailing/Model/ContentTokenInterface.php new file mode 100644 index 00000000..13db8a36 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/ContentTokenInterface.php @@ -0,0 +1,62 @@ +name = $name; + $this->dataType = $dataType; + $this->template = $template; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getDataType(): ContentTokenDataType + { + return $this->dataType; + } + + /** + * {@inheritdoc} + */ + public function getTemplate(): MessageTemplateInterface + { + return $this->template; + } +} diff --git a/src/Sil/Component/Emailing/Model/ContentTokenTypeInterface.php b/src/Sil/Component/Emailing/Model/ContentTokenTypeInterface.php new file mode 100644 index 00000000..a9914f09 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/ContentTokenTypeInterface.php @@ -0,0 +1,37 @@ +isValid($value)) { + throw new InvalidArgumentException(sprintf('The string « %s » is not a valid email address', $value)); + } + + $this->value = $value; + } + + /** + * Validate if given string is an email. + * + * @param string $value + */ + public function isValid(?string $value = null): bool + { + if ($value === null) { + $value = $this->value; + } + + return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; + } + + /** + * Gets the email as string. + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->getValue(); + } +} diff --git a/src/Sil/Component/Emailing/Model/EmailAddressInterface.php b/src/Sil/Component/Emailing/Model/EmailAddressInterface.php new file mode 100644 index 00000000..8159d93e --- /dev/null +++ b/src/Sil/Component/Emailing/Model/EmailAddressInterface.php @@ -0,0 +1,37 @@ +lists = new ArrayCollection(); + } + + /** + * @return array|MailingListInterface[] + */ + public function getLists(): array + { + return $this->lists->getValues(); + } + + /** + * @param MailingListInterface $list + * + * @throws InvalidArgumentException + */ + public function addList(MailingListInterface $list): void + { + if ($this->lists->contains($list)) { + throw new InvalidArgumentException(sprintf('List « %s » is already used by message « %s »', $list->getName(), $this->getTitle())); + } + $this->lists->add($list); + } + + /** + * @param MailingListInterface $list + * + * @throws InvalidArgumentException + */ + public function removeList(MailingListInterface $list): void + { + if (!$this->lists->contains($list)) { + throw new InvalidArgumentException(sprintf('List « %s » is not used by message « %s »', $list->getName(), $this->getTitle())); + } + $this->lists->removeElement($list); + } +} diff --git a/src/Sil/Component/Emailing/Model/GroupedMessageInterface.php b/src/Sil/Component/Emailing/Model/GroupedMessageInterface.php new file mode 100644 index 00000000..21637b10 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/GroupedMessageInterface.php @@ -0,0 +1,37 @@ +name = $name; + $this->description = $description; + + $this->recipients = new ArrayCollection(); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * {@inheritdoc} + */ + public function setDescription(?string $description): void + { + $this->description = $description; + } + + /** + * {@inheritdoc} + */ + public function setEnabled(bool $enabled): void + { + if ($this->recipients->count() === 0) { + throw new DomainException(sprintf('MailingList « %s » cannot be enabled because it has no recipients', $this->getName())); + } + $this->enabled = $enabled; + } + + /** + * {@inheritdoc} + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function getRecipients(): array + { + return $this->recipients->getValues(); + } + + /** + * {@inheritdoc} + */ + public function addRecipient(RecipientInterface $recipient): void + { + if ($this->recipients->contains($recipient)) { + throw new InvalidArgumentException(sprintf('Recipient « %s » already belong to list « %s »', $recipient->getEmail(), $this->getName())); + } + $this->recipients->add($recipient); + } + + /** + * {@inheritdoc} + */ + public function removeRecipient(RecipientInterface $recipient): void + { + if (!$this->recipients->contains($recipient)) { + throw new InvalidArgumentException(sprintf('Recipient « %s » does not belongs to list « %s »', $recipient->getEmail(), $this->getName())); + } + $this->recipients->removeElement($recipient); + } +} diff --git a/src/Sil/Component/Emailing/Model/MailingListInterface.php b/src/Sil/Component/Emailing/Model/MailingListInterface.php new file mode 100644 index 00000000..40ce42f8 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/MailingListInterface.php @@ -0,0 +1,80 @@ +setValue($value); + $this->stateMachine = new MessageStateMachine($this); + } + + /** + * {@inheritdoc} + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @internal + * + * @param string $value + */ + public function setValue(string $value): void + { + if (!in_array($value, static::getStates())) { + throw new InvalidArgumentException(sprintf('The state %s is not a valid state, valids are : %s', $value, implode(', ', static::getStates()))); + } + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public static function getStates(): array + { + return [ + 'DRAFT' => static::DRAFT, + 'VALIDATED' => static::VALIDATED, + 'SENT' => static::SENT, + 'DELETED' => static::DELETED, + ]; + } + + /** + * {@inheritdoc} + */ + public static function draft(): MessageStateInterface + { + return new self(static::DRAFT); + } + + /** + * {@inheritdoc} + */ + public static function validated(): MessageStateInterface + { + return new self(static::VALIDATED); + } + + /** + * {@inheritdoc} + */ + public static function sent(): MessageStateInterface + { + return new self(static::SENT); + } + + /** + * {@inheritdoc} + */ + public static function deleted(): MessageStateInterface + { + return new self(static::DELETED); + } + + /** + * {@inheritdoc} + */ + public function isDraft(): bool + { + return $this->value === static::DRAFT; + } + + /** + * {@inheritdoc} + */ + public function isValidated(): bool + { + return $this->value === static::VALIDATED; + } + + /** + * {@inheritdoc} + */ + public function isSent(): bool + { + return $this->value === static::SENT; + } + + /** + * {@inheritdoc} + */ + public function isDeleted(): bool + { + return $this->value === static::DELETED; + } + + /** + * {@inheritdoc} + */ + public function toDelete(): MessageStateInterface + { + $this->stateMachine->apply('delete'); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function toValidate(): MessageStateInterface + { + $this->stateMachine->apply('validate'); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function toSent(): MessageStateInterface + { + $this->stateMachine->apply('sent'); + + return $this; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Sil/Component/Emailing/Model/MessageStateAwareInterface.php b/src/Sil/Component/Emailing/Model/MessageStateAwareInterface.php new file mode 100644 index 00000000..e22cb867 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/MessageStateAwareInterface.php @@ -0,0 +1,66 @@ +state; + } + + /** + * @internal + * + * @param MessageStateInterface $state + */ + public function setState(MessageStateInterface $state): void + { + $this->state = $state; + } + + /** + * Sets current state to validated. + */ + public function beValidated(): void + { + $this->getState()->toValidate(); + } + + /** + * Sets current state to sent. + */ + public function beSent(): void + { + $this->getState()->toSent(); + } + + /** + * Sets current state to deleted. + */ + public function beDeleted(): void + { + $this->getState()->toDelete(); + } + + /** + * Checks if current state is draft. + * + * @return bool + */ + public function isDraft(): bool + { + return $this->getState()->isDraft(); + } + + /** + * Checks if current state is validated. + * + * @return bool + */ + public function isValidated(): bool + { + return $this->getState()->isValidated(); + } + + /** + * Checks if current state is sent. + * + * @return bool + */ + public function isSent(): bool + { + return $this->getState()->isSent(); + } + + /** + * Checks if current state is deleted. + * + * @return bool + */ + public function isDeleted(): bool + { + return $this->getState()->isDeleted(); + } +} diff --git a/src/Sil/Component/Emailing/Model/MessageStateInterface.php b/src/Sil/Component/Emailing/Model/MessageStateInterface.php new file mode 100644 index 00000000..6891ddc6 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/MessageStateInterface.php @@ -0,0 +1,107 @@ + static::STATE, + * [...] + * ]. + * + * @return array + */ + public static function getStates(): array; + + /** + * Gets current state value. + * + * @return string + */ + public function getValue(): string; + + /** + * Draft state constructor. + * + * @return MessageStateInterface + */ + public static function draft(): self; + + /** + * Validated state constructor. + * + * @return MessageStateInterface + */ + public static function validated(): self; + + /** + * Sent state constructor. + * + * @return MessageStateInterface + */ + public static function sent(): self; + + /** + * Deleted state constructor. + * + * @return MessageStateInterface + */ + public static function deleted(): self; + + /** + * Current state is Draft. + * + * @return bool + */ + public function isDraft(): bool; + + /** + * Current state is Validated. + * + * @return bool + */ + public function isValidated(): bool; + + /** + * Current state is Sent. + * + * @return bool + */ + public function isSent(): bool; + + /** + * Current state is Deleted. + * + * @return bool + */ + public function isDeleted(): bool; + + /** + * Apply transition delete. + */ + public function toDelete(): self; + + /** + * Apply transition validate. + */ + public function toValidate(): self; + + /** + * Apply transition cancel. + */ + public function toSent(): self; +} diff --git a/src/Sil/Component/Emailing/Model/MessageTemplate.php b/src/Sil/Component/Emailing/Model/MessageTemplate.php new file mode 100644 index 00000000..33311f48 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/MessageTemplate.php @@ -0,0 +1,138 @@ +name = $name; + $this->content = $content; + + $this->tokenTypes = new ArrayCollection(); + $this->messages = new ArrayCollection(); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getContent(): string + { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function getTokenTypes(): array + { + return $this->tokenTypes->getValues(); + } + + /** + * {@inheritdoc} + */ + public function addTokenType(ContentTokenTypeInterface $token): void + { + if ($this->tokenTypes->contains($token)) { + throw new InvalidArgumentException(sprintf('TokenType « %s » is already associated to template « %s »', $token->getName(), $this->getName())); + } + $this->tokenTypes->add($token); + } + + /** + * {@inheritdoc} + */ + public function removeTokenType(ContentTokenTypeInterface $token): void + { + if (!$this->tokenTypes->contains($token)) { + throw new InvalidArgumentException(sprintf('TokenType « %s » is not associated to template « %s »', $token->getName(), $this->getName())); + } + $this->tokenTypes->removeElement($token); + } + + /** + * @return array|MessageInterface[] + */ + public function getMessages(): array + { + return $this->messages->getValues(); + } + + /** + * @param MessageInterface $message + * + * @throws InvalidArgumentException + */ + public function addMessage(MessageInterface $message): void + { + if ($this->messages->contains($message)) { + throw new InvalidArgumentException(sprintf('Message « %s » is already using template « %s »', $message->getTitle(), $this->getName())); + } + $this->messages->add($message); + } + + /** + * @param MessageInterface $message + * + * @throws InvalidArgumentException + */ + public function removeMessage(MessageInterface $message): void + { + if (!$this->messages->contains($message)) { + throw new InvalidArgumentException(sprintf('Message « %s » is not using template « %s »', $message->getTitle(), $this->getName())); + } + $this->messages->removeElement($message); + } +} diff --git a/src/Sil/Component/Emailing/Model/MessageTemplateInterface.php b/src/Sil/Component/Emailing/Model/MessageTemplateInterface.php new file mode 100644 index 00000000..2c79abfb --- /dev/null +++ b/src/Sil/Component/Emailing/Model/MessageTemplateInterface.php @@ -0,0 +1,57 @@ +email = $email; + } + + /** + * {@inheritdoc} + */ + public static function createFromEmailAsString(string $email): RecipientInterface + { + $email = new EmailAddress($email); + + return new self($email); + } + + /** + * {@inheritdoc} + */ + public function isValid(): bool + { + return $this->valid; + } + + /** + * {@inheritdoc} + */ + public function setValid(bool $valid): void + { + $this->valid = $valid; + } + + /** + * {@inheritdoc} + */ + public function getEmail(): EmailAddressInterface + { + return $this->email; + } +} diff --git a/src/Sil/Component/Emailing/Model/RecipientInterface.php b/src/Sil/Component/Emailing/Model/RecipientInterface.php new file mode 100644 index 00000000..c885a2c7 --- /dev/null +++ b/src/Sil/Component/Emailing/Model/RecipientInterface.php @@ -0,0 +1,46 @@ +to = new ArrayCollection(); + $this->cc = new ArrayCollection(); + $this->bcc = new ArrayCollection(); + + $this->from = $from; + $this->addTo($to); + } + + /** + * {@inheritdoc} + */ + public function getFrom(): EmailAddressInterface + { + return $this->from; + } + + /** + * {@inheritdoc} + */ + public function getTo(): array + { + return $this->to->getValues(); + } + + /** + * {@inheritdoc} + */ + public function addTo(EmailAddressInterface $to): void + { + if ($this->to->contains($to)) { + throw new InvalidArgumentException(sprintf('To « %s » is already set for message « %s »', $to, $this->getTitle())); + } + $this->to->add($to); + } + + /** + * {@inheritdoc} + */ + public function removeTo(EmailAddressInterface $to): void + { + if (!$this->to->contains($to)) { + throw new InvalidArgumentException(sprintf('To « %s » is not set for message « %s »', $to, $this->getTitle())); + } + if ($this->to->count() === 1) { + throw new DomainException( + sprintf( + 'Message « %s » must have at least one recipient in To field. + Removing « %s » will result in an empty to field, wich is not a valid state for a message', + $this->getTitle(), + $to + ) + ); + } + $this->to->removeElement($to); + } + + /** + * {@inheritdoc} + */ + public function getCc(): array + { + return $this->cc->getValues(); + } + + /** + * {@inheritdoc} + */ + public function addCc(EmailAddressInterface $cc): void + { + if ($this->cc->contains($cc)) { + throw new InvalidArgumentException(sprintf('Cc address « %s » is already set for message « %s »', $cc, $this->getTitle())); + } + $this->cc->add($cc); + } + + /** + * {@inheritdoc} + */ + public function removeCc(EmailAddressInterface $cc): void + { + if (!$this->cc->contains($cc)) { + throw new InvalidArgumentException(sprintf('Cc address « %s » does not exists for message « %s »', $cc, $this->getTitle())); + } + $this->cc->removeElement($cc); + } + + /** + * {@inheritdoc} + */ + public function getBcc(): array + { + return $this->bcc->getValues(); + } + + /** + * {@inheritdoc} + */ + public function addBcc(EmailAddressInterface $bcc): void + { + if ($this->bcc->contains($bcc)) { + throw new InvalidArgumentException(sprintf('Bcc address « %s » is already set for message « %s »', $bcc, $this->getTitle())); + } + $this->bcc->add($bcc); + } + + /** + * {@inheritdoc} + */ + public function removeBcc(EmailAddressInterface $bcc): void + { + if (!$this->bcc->contains($bcc)) { + throw new InvalidArgumentException(sprintf('Bcc address « %s » does not exists for message « %s »', $bcc, $this->getTitle())); + } + $this->bcc->removeElement($bcc); + } +} diff --git a/src/Sil/Component/Emailing/Model/SimpleMessageInterface.php b/src/Sil/Component/Emailing/Model/SimpleMessageInterface.php new file mode 100644 index 00000000..7641ab3d --- /dev/null +++ b/src/Sil/Component/Emailing/Model/SimpleMessageInterface.php @@ -0,0 +1,98 @@ +clearTokens(); + $message->setTemplate($template); + $message->setContent($template->getContent()); + + foreach ($template->getTokenTypes() as $tokenType) { + $token = new ContentToken($message, $tokenType); + $message->addToken($token); + } + } + + /** + * Replaces all token placeholder with values (if they are set) in message content. + * + * @param MessageInterface $message + */ + public function replaceTokensOfMessage(MessageInterface $message): void + { + foreach ($message->getTokens() as $token) { + if ($token->getValue() === null) { + continue; + } + + $rawContent = $message->getContent(); + + $regexExpression = sprintf('/%s/', preg_quote('%%' . $token->getName() . '%%')); + + $rawContent = preg_replace($regexExpression, $token->getValueAsString(), $rawContent); + + $message->setContent($rawContent); + } + } +} diff --git a/src/Sil/Component/Emailing/StateMachine/MessageStateMachine.php b/src/Sil/Component/Emailing/StateMachine/MessageStateMachine.php new file mode 100644 index 00000000..96084eb4 --- /dev/null +++ b/src/Sil/Component/Emailing/StateMachine/MessageStateMachine.php @@ -0,0 +1,49 @@ + 'message_state', + 'property_path' => 'value', + 'states' => [ + MessageState::DRAFT, + MessageState::VALIDATED, + MessageState::SENT, + MessageState::DELETED, + ], + 'transitions' => [ + 'delete' => [ + 'from' => [MessageState::DRAFT, MessageState::VALIDATED], + 'to' => MessageState::DELETED, + ], + 'validate' => [ + 'from' => [MessageState::DRAFT], + 'to' => MessageState::VALIDATED, + ], + 'sent' => [ + 'from' => [MessageState::VALIDATED, MessageState::SENT], + 'to' => MessageState::SENT, + ], + ], + ]; + + public function __construct($object) + { + parent::__construct($object, $this->config); + } +} diff --git a/src/Sil/Component/Emailing/Tests/.gitkeep b/src/Sil/Component/Emailing/Tests/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Sil/Component/Emailing/Tests/Unit.suite.yml b/src/Sil/Component/Emailing/Tests/Unit.suite.yml new file mode 100644 index 00000000..026c91db --- /dev/null +++ b/src/Sil/Component/Emailing/Tests/Unit.suite.yml @@ -0,0 +1,9 @@ +# Codeception Test Suite Configuration +# +# Suite for unit or integration tests. + +actor: UnitTester +modules: + enabled: + - Asserts + - \Helper\Unit \ No newline at end of file diff --git a/src/Sil/Component/Emailing/Tests/Unit/.gitkeep b/src/Sil/Component/Emailing/Tests/Unit/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Sil/Component/Emailing/Tests/Unit/EmailAddressTest.php b/src/Sil/Component/Emailing/Tests/Unit/EmailAddressTest.php new file mode 100644 index 00000000..c3a5188e --- /dev/null +++ b/src/Sil/Component/Emailing/Tests/Unit/EmailAddressTest.php @@ -0,0 +1,41 @@ +fixtures = new Fixtures(); + } + + public function test_valid_email_address_creation() + { + $emailAddress = new EmailAddress('valid@sil.eu'); + + $this->assertTrue($emailAddress->isValid()); + } + + public function test_invalid_email_address_creation() + { + $this->expectException(\InvalidArgumentException::class); + + $emailAddress = new EmailAddress('not.a.valid.address'); + } +} diff --git a/src/Sil/Component/Emailing/Tests/Unit/Fixtures/Fixtures.php b/src/Sil/Component/Emailing/Tests/Unit/Fixtures/Fixtures.php new file mode 100644 index 00000000..6095483f --- /dev/null +++ b/src/Sil/Component/Emailing/Tests/Unit/Fixtures/Fixtures.php @@ -0,0 +1,366 @@ +rawData = [ + 'Templates' => [ + 'basic' => [ + 'name' => 'basic', + 'content' => 'A template without token placeholder', + 'tokens' => [], + ], + 'rich' => [ + 'name' => 'rich', + 'content' => 'A template with token placeholders : + - %%A_BOOLEAN_TOKEN%% + - %%A_STRING_TOKEN%% + - %%A_INTEGER_TOKEN%% + - %%A_FLOAT_TOKEN%% + - %%A_DATE_TOKEN%% + - %%A_DATETIME_TOKEN%% + ', + 'tokens' => [ + [ + 'name' => 'A_BOOLEAN_TOKEN', + 'type' => 'BOOLEAN', + ], + [ + 'name' => 'A_STRING_TOKEN', + 'type' => 'STRING', + ], + [ + 'name' => 'A_INTEGER_TOKEN', + 'type' => 'INTEGER', + ], + [ + 'name' => 'A_FLOAT_TOKEN', + 'type' => 'FLOAT', + ], + [ + 'name' => 'A_DATE_TOKEN', + 'type' => 'DATE', + ], + [ + 'name' => 'A_DATETIME_TOKEN', + 'type' => 'DATETIME', + ], + ], + ], + ], + 'Mailing lists' => [ + 'ListOne' => [ + 'name' => 'ListOne', + 'description' => null, + 'recipients' => [ + 'recipient1@sil.eu', + 'recipient2@sil.eu', + 'recipient3@sil.eu', + 'recipient4@sil.eu', + 'recipient5@sil.eu', + ], + ], + 'ListTwo' => [ + 'name' => 'ListTwo', + 'description' => 'A small description of list', + 'recipients' => [ + 'recipient6@sil.eu', + 'recipient7@sil.eu', + 'recipient8@sil.eu', + 'recipient9@sil.eu', + 'recipient10@sil.eu', + ], + ], + ], + 'Simple messages' => [ + [ + 'title' => 'Simple message One', + 'content' => 'A rich content without lorem.', + 'template' => 'rich', + 'from' => 'sender1@sil.eu', + 'to' => ['recipient1@sil.eu', 'recipient2@sil.eu'], + 'cc' => ['cc1@sil.eu', 'cc2@sil.eu', 'cc3@sil.eu', 'cc4@sil.eu'], + 'bcc' => ['bcc1@sil.eu'], + ], + [ + 'title' => 'Simple message Two', + 'content' => 'A text content without lorem.', + 'template' => null, + 'from' => 'sender2@sil.eu', + 'to' => ['recipient3@sil.eu', 'recipient4@sil.eu'], + 'cc' => [], + 'bcc' => [], + ], + ], + 'Grouped messages' => [ + [ + 'title' => 'Grouped message One', + 'content' => 'A rich content without lorem.', + 'template' => 'basic', + 'lists' => [ + 'ListOne', + ], + ], + [ + 'title' => 'Grouped message Two', + 'content' => 'A text content without lorem.', + 'template' => null, + 'lists' => [ + 'ListOne', + 'ListTwo', + ], + ], + ], + ]; + + $this->simpleMessageRepository = new InMemoryRepository(SimpleMessageInterface::class); + $this->groupedMessageRepository = new InMemoryRepository(GroupedMessageInterface::class); + $this->mailingListRepository = new InMemoryRepository(MailingListInterface::class); + $this->recipientRepository = new InMemoryRepository(RecipientInterface::class); + $this->emailAddressRepository = new InMemoryRepository(EmailAddressInterface::class); + $this->messageTemplateRepository = new InMemoryRepository(MessageTemplateInterface::class); + $this->contentTokenTypeRepository = new InMemoryRepository(ContentTokenTypeInterface::class); + $this->contentTokenRepository = new InMemoryRepository(ContentTokenInterface::class); + + $this->generateFixtures(); + } + + private function generateFixtures(): void + { + $this->loadTemplates(); + $this->loadMailingLists(); + $this->loadSimpleMessages(); + $this->loadGroupedMessages(); + } + + private function loadtemplates() + { + foreach ($this->rawData['Templates'] as $templateData) { + $template = new MessageTemplate($templateData['name'], $templateData['content']); + + foreach ($templateData['tokens'] as $tokenData) { + $tokenType = new ContentTokenType($template, $tokenData['name'], new ContentTokenDataType(constant(ContentTokenDataType::class . '::TYPE_' . $tokenData['type']))); + $template->addTokenType($tokenType); + $this->contentTokenTypeRepository->add($tokenType); + } + + $this->messageTemplateRepository->add($template); + } + } + + private function loadMailingLists() + { + foreach ($this->rawData['Mailing lists'] as $mailingListData) { + $mailingList = new MailingList($mailingListData['name']); + + foreach ($mailingListData['recipients'] as $recipientEmail) { + $recipient = Recipient::createFromEmailAsString($recipientEmail); + $this->recipientRepository->add($recipient); + $mailingList->addRecipient($recipient); + } + + $this->mailingListRepository->add($mailingList); + } + } + + private function loadSimpleMessages() + { + foreach ($this->rawData['Simple messages'] as $messageData) { + $from = new EmailAddress($messageData['from']); + $to = new EmailAddress($messageData['to'][0]); + + $this->emailAddressRepository->add($from); + $this->emailAddressRepository->add($to); + + $simpleMessage = new SimpleMessage($messageData['title'], $messageData['content'], $from, $to); + + if ($messageData['template'] !== null) { + $template = $this->messageTemplateRepository->findOneBy(['name' => $messageData['template']]); + + $templateHandler = new TemplateHandler(); + $templateHandler->applyTemplateToMessage($template, $simpleMessage); + } + + foreach ($messageData['to'] as $tos) { + $toAddress = new EmailAddress($tos); + if (!in_array($toAddress, $simpleMessage->getTo())) { + $simpleMessage->addTo($toAddress); + $this->emailAddressRepository->add($toAddress); + } + } + + foreach ($messageData['cc'] as $ccs) { + $ccAddress = new EmailAddress($ccs); + if (!in_array($ccAddress, $simpleMessage->getCc())) { + $simpleMessage->addCc($ccAddress); + $this->emailAddressRepository->add($ccAddress); + } + } + + foreach ($messageData['bcc'] as $bccs) { + $bccAddress = new EmailAddress($bccs); + if (!in_array($bccAddress, $simpleMessage->getBcc())) { + $simpleMessage->addBcc($bccAddress); + $this->emailAddressRepository->add($bccAddress); + } + } + + $this->simpleMessageRepository->add($simpleMessage); + } + } + + private function loadGroupedMessages() + { + foreach ($this->rawData['Grouped messages'] as $messageData) { + $groupedMessage = new GroupedMessage($messageData['title'], $messageData['content']); + + if ($messageData['template'] !== null) { + $template = $this->messageTemplateRepository->findOneBy(['name' => $messageData['template']]); + + $templateHandler = new TemplateHandler(); + $templateHandler->applyTemplateToMessage($template, $groupedMessage); + } + + foreach ($messageData['lists'] as $listName) { + $list = $this->mailingListRepository->findOneBy(['name' => $listName]); + $groupedMessage->addList($list); + } + + $this->groupedMessageRepository->add($groupedMessage); + } + } + + /** + * @return InMemoryRepository + */ + public function getSimpleMessageRepository(): InMemoryRepository + { + return $this->simpleMessageRepository; + } + + /** + * @return InMemoryRepository + */ + public function getGroupedMessageRepository(): InMemoryRepository + { + return $this->groupedMessageRepository; + } + + /** + * @return InMemoryRepository + */ + public function getMailingListRepository(): InMemoryRepository + { + return $this->mailingListRepository; + } + + /** + * @return InMemoryRepository + */ + public function getRecipientRepository(): InMemoryRepository + { + return $this->recipientRepository; + } + + /** + * @return InMemoryRepository + */ + public function getMessageTemplateRepository(): InMemoryRepository + { + return $this->messageTemplateRepository; + } + + /** + * @return InMemoryRepository + */ + public function getContentTokenTypeRepository(): InMemoryRepository + { + return $this->contentTokenTypeRepository; + } + + /** + * @return InMemoryRepository + */ + public function getContentTokenRepository(): InMemoryRepository + { + return $this->contentTokenRepository; + } + + /** + * @return array + */ + public function getRawData(): array + { + return $this->rawData; + } +} diff --git a/src/Sil/Component/Emailing/Tests/Unit/MailingListTest.php b/src/Sil/Component/Emailing/Tests/Unit/MailingListTest.php new file mode 100644 index 00000000..b92de679 --- /dev/null +++ b/src/Sil/Component/Emailing/Tests/Unit/MailingListTest.php @@ -0,0 +1,55 @@ +fixtures = new Fixtures(); + } + + public function test_mailing_list_creation() + { + $mailingList = new MailingList('Test list', 'a test mailing list'); + + $this->assertEquals('Test list', $mailingList->getName()); + $this->assertEquals('a test mailing list', $mailingList->getDescription()); + $this->assertFalse($mailingList->isEnabled()); + } + + public function test_mailing_list_enabling_without_recipients() + { + $mailingList = new MailingList('Test list', 'a test mailing list'); + + $this->expectException(\DomainException::class); + + $mailingList->setEnabled(true); + } + + public function test_mailing_list_enabling_with_recipients() + { + $mailingList = new MailingList('Test list', 'a test mailing list'); + + $mailingList->addRecipient(Recipient::createFromEmailAsString('recipient@sil.eu')); + + $mailingList->setEnabled(true); + } +} diff --git a/src/Sil/Component/Emailing/Tests/Unit/MessageTemplateTest.php b/src/Sil/Component/Emailing/Tests/Unit/MessageTemplateTest.php new file mode 100644 index 00000000..f3d216f2 --- /dev/null +++ b/src/Sil/Component/Emailing/Tests/Unit/MessageTemplateTest.php @@ -0,0 +1,99 @@ +fixtures = new Fixtures(); + } + + public function test_applying_template_with_template_handler_to_a_new_message() + { + $from = new EmailAddress('from@sil.eu'); + $to = new EmailAddress('to@sil.eu'); + + $templateData = $this->fixtures->getRawData()['Templates']['rich']; + $template = $this->fixtures->getMessageTemplateRepository()->findOneBy(['name' => $templateData['name']]); + + $message = new SimpleMessage('Simple message', 'a simple message test', $from, $to); + + $templateHandler = new TemplateHandler(); + $templateHandler->applyTemplateToMessage($template, $message); + + $this->assertEquals(count($template->getTokenTypes()), count($message->getTokens())); + + foreach ($message->getTokens() as $token) { + $this->assertContains($token->getTokenType(), $template->getTokenTypes()); + } + } + + public function test_clearing_message_tokens() + { + $messageData = $this->fixtures->getRawData()['Simple messages'][0]; + $message = $this->fixtures->getSimpleMessageRepository()->findOneBy(['title' => $messageData['title']]); + + $this->assertEquals(6, count($message->getTokens())); + + $message->clearTokens(); + + $this->assertEquals(0, count($message->getTokens())); + } + + public function test_replacing_tokens_on_message() + { + $messageData = $this->fixtures->getRawData()['Simple messages'][0]; + $templateData = $this->fixtures->getRawData()['Templates'][$messageData['template']]; + $message = $this->fixtures->getSimpleMessageRepository()->findOneBy(['title' => $messageData['title']]); + + $tokenValues = [ + ContentTokenDataType::TYPE_BOOLEAN => true, + ContentTokenDataType::TYPE_DATE => new DateTime('2020-05-01'), + ContentTokenDataType::TYPE_DATETIME => new DateTime('2020-05-01 12:12:42'), + ContentTokenDataType::TYPE_STRING => 'a replaced string', + ContentTokenDataType::TYPE_INTEGER => 42, + ContentTokenDataType::TYPE_FLOAT => 512.42, + ]; + + foreach ($message->getTokens() as $token) { + $token->setValue($tokenValues[$token->getTokenType()->getDataType()->getValue()]); + } + + $this->assertEquals(trim($templateData['content']), $message->getContent()); + + $templateHandler = new TemplateHandler(); + $templateHandler->replaceTokensOfMessage($message); + + $expectedContent = trim('A template with token placeholders : + - Yes + - a replaced string + - 42 + - 512.42 + - 2020-05-01 + - 2020-05-01 12:12:42 + '); + + $this->assertEquals($expectedContent, $message->getContent()); + } +} diff --git a/src/Sil/Component/Emailing/Tests/Unit/MessageTest.php b/src/Sil/Component/Emailing/Tests/Unit/MessageTest.php new file mode 100644 index 00000000..ae4cef63 --- /dev/null +++ b/src/Sil/Component/Emailing/Tests/Unit/MessageTest.php @@ -0,0 +1,49 @@ +fixtures = new Fixtures(); + } + + public function test_simple_email_creation() + { + $from = new EmailAddress('from@sil.eu'); + $to = new EmailAddress('to@sil.eu'); + $message = new SimpleMessage('Simple message', 'a simple message test', $from, $to); + + $this->assertEquals('Simple message', $message->getTitle()); + $this->assertEquals('a simple message test', $message->getContent()); + $this->assertEquals($from->getValue(), $message->getFrom()->getValue()); + $this->assertContains($to, $message->getTo()); + } + + public function test_grouped_message_creation() + { + $message = new GroupedMessage('Grouped message', 'a grouped message test'); + + $this->assertEquals('Grouped message', $message->getTitle()); + $this->assertEquals('a grouped message test', $message->getContent()); + } +} diff --git a/src/Sil/Component/Emailing/Tests/Unit/RecipientTest.php b/src/Sil/Component/Emailing/Tests/Unit/RecipientTest.php new file mode 100644 index 00000000..8f1f3e73 --- /dev/null +++ b/src/Sil/Component/Emailing/Tests/Unit/RecipientTest.php @@ -0,0 +1,51 @@ +fixtures = new Fixtures(); + } + + public function test_recipient_creation() + { + $emailAddress = new EmailAddress('recipient@sil.eu'); + + $recipient = new Recipient($emailAddress); + + $this->assertEquals($emailAddress, $recipient->getEmail()); + } + + public function test_recipient_creation_from_valid_string() + { + $recipient = Recipient::createFromEmailAsString('valid.recipient@sil.eu'); + + $this->assertEquals('valid.recipient@sil.eu', $recipient->getEmail()->getValue()); + } + + public function test_recipient_creation_from_invalid_string() + { + $this->expectException(\InvalidArgumentException::class); + + $recipient = Recipient::createFromEmailAsString('invalid.recipient'); + } +} diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/after_success_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/after_success_test.sh new file mode 100644 index 00000000..87158518 --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/after_success_test.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +set -ev + +coveralls -v diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/before_install_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_test.sh new file mode 100644 index 00000000..e4fc977b --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +set -ev + +composer self-update --stable + +mkdir -p ${HOME}/bin + +# Coveralls client install +wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar --output-document="${HOME}/bin/coveralls" +chmod u+x "${HOME}/bin/coveralls" diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/before_install_travis_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_travis_test.sh new file mode 100644 index 00000000..5e838f77 --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_travis_test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -ev + +mkdir --parents "${HOME}/bin" + +if [ "${WHORUN}" = travis ] +then + # Ugly hack + echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini + +fi diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/create_database_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/create_database_test.sh new file mode 100644 index 00000000..4415f6af --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/create_database_test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -v + +# TODO share this between script (in an include) +if [ -f .env ] +then + source .env +else + echo "Please run this script from project root, and check .env file as it is mandatory" + echo "If it is missing a quick solution is :" + echo "ln -s .env.travis .env" + exit 42 +fi + +if [ -z "${DBHOST}" ] +then + echo "Please add DBHOST in .env file as it is mandatory" + exit 42 +fi + + +# Database creation + +### +### mysql +### + +# (mysql service is started by default by travis for each build instance +# (mysql travis user is created by travis for each build instance +# mysql -u travis -e 'CREATE DATABASE travis;' -v + +### +### postgresql +### + +#psql auto password +#echo localhost:5432:*:postgres:postgres24 >> ~/.pgpass + +# needed in .travis.yml +#services: +# - postgresql +# or here : sudo /etc/init.d/postgresql start + + +psql -w -h ${DBHOST} -c "DROP DATABASE IF EXISTS ${DBAPPNAME};" -U ${DBROOTUSER} +psql -w -h ${DBHOST} -c "DROP ROLE IF EXISTS ${DBAPPUSER};" -U ${DBROOTUSER} + + +# (we try to create a travis user) +psql -w -h ${DBHOST} -c "CREATE USER ${DBAPPUSER} WITH PASSWORD '${DBAPPPASSWORD}';" -U ${DBROOTUSER} +psql -w -h ${DBHOST} -c "ALTER ROLE ${DBAPPUSER} WITH CREATEDB;" -U ${DBROOTUSER} + +psql -w -h ${DBHOST} -c "CREATE DATABASE ${DBAPPNAME};" -U ${DBROOTUSER} +psql -w -h ${DBHOST} -c "ALTER DATABASE ${DBAPPNAME} OWNER TO ${DBAPPUSER};" -U ${DBROOTUSER} + + +psql -w -h ${DBHOST} -c 'CREATE EXTENSION "uuid-ossp";' -U ${DBROOTUSER} -d ${DBAPPNAME} + diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/do_run.sh b/src/Sil/Component/Emailing/bin/ci-scripts/do_run.sh new file mode 100644 index 00000000..0411937d --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/do_run.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +for name in $@ +do + script_name=$(dirname $0)/$name + if [ -x $script_name ] + then + $script_name + fi +done diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/install_docs.sh b/src/Sil/Component/Emailing/bin/ci-scripts/install_docs.sh new file mode 100644 index 00000000..362f6449 --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/install_docs.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +set -ev + +pip install -r Resources/doc/requirements.txt --user diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/install_lint.sh b/src/Sil/Component/Emailing/bin/ci-scripts/install_lint.sh new file mode 100644 index 00000000..42a58dca --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/install_lint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +set -ev + +composer global require sllh/composer-lint:@stable --prefer-dist --no-interaction + diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/install_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/install_test.sh new file mode 100644 index 00000000..483ed38e --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/install_test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh +set -ev + +composer install --no-interaction --prefer-dist + + + + + diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/run_docs.sh b/src/Sil/Component/Emailing/bin/ci-scripts/run_docs.sh new file mode 100644 index 00000000..31fae91f --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/run_docs.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +cd Resources/doc +sphinx-build -b html -d _build/doctrees . _build/html diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/run_lint.sh b/src/Sil/Component/Emailing/bin/ci-scripts/run_lint.sh new file mode 100644 index 00000000..732ea402 --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/run_lint.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +composer validate diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/run_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/run_test.sh new file mode 100644 index 00000000..625ff2a5 --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/run_test.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -ev + +# TODO share this between script (in an include) +if [ -f .env ] +then + source .env +else + echo "Please run this script from project root, and check .env file as it is mandatory" + echo "If it is missing a quick solution is :" + echo "ln -s .env.travis .env" + exit 42 +fi + +export SILURL + + +if [ -n "$PHPUNITCMD" ] +then + $PHPUNITCMD +fi + +#bin/ci-scripts/do_it_for_bundle.sh run test diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/set_db_host_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/set_db_host_test.sh new file mode 100644 index 00000000..f9d82167 --- /dev/null +++ b/src/Sil/Component/Emailing/bin/ci-scripts/set_db_host_test.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -ev + +# TODO share this between script (in an include) +if [ -f .env ] +then + source .env +else + echo "Please run this script from project root, and check .env file as it is mandatory" + echo "If it is missing a quick solution is :" + echo "ln -s .env.travis .env" + exit 42 +fi + + + + +################## +#### POSTGRES #### + + +# TODO +# should remove db info from app/config/config_test.yml +# and use sed or etcd or confd or both to update parameters.yml.dist or create parameters.yml +# sed -e s/'database_host: 127.0.0.1'/'database_host: ${DBHOST}'/g -i app/config/parameters.yml.dist +if [ -n "${DBHOST}" ] +then + + if [ -f Tests/Resources/App/config/parameters.yml ] + then + sed -e s/'127.0.0.1'/${DBHOST}/g -i Tests/Resources/App/config/parameters.yml + sed -e s/'blast_test_user'/${DBAPPUSER}/g -i Tests/Resources/App/config/parameters.yml + sed -e s/'blast_test_password'/${DBAPPPASSWORD}/g -i Tests/Resources/App/config/parameters.yml + sed -e s/'blast_test_db'/${DBAPPNAME}/g -i Tests/Resources/App/config/parameters.yml + cat Tests/Resources/App/config/parameters.yml + fi + + #TODO + # should use env var from etcd (for password) + echo ${DBHOST}:5432:*:${DBROOTUSER}:${DBROOTPASSWORD} >> $HOME/.pgpass + echo ${DBHOST}:5432:*:${DBROOTUSER}:${DBROOTPASSWORD} >> $HOME/.pgpass + chmod 600 $HOME/.pgpass + cat $HOME/.pgpass +fi diff --git a/src/Sil/Component/Emailing/composer.json b/src/Sil/Component/Emailing/composer.json new file mode 100644 index 00000000..eb711009 --- /dev/null +++ b/src/Sil/Component/Emailing/composer.json @@ -0,0 +1,40 @@ +{ + "name": "sil-project/emailing", + "type": "library", + "description": "Emailing Management", + "require": { + "php": "^7.1", + "doctrine/collections": "^1.3", + "blast-project/resource": "self.version", + "winzou/state-machine": "^0.3" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "license": "LGPL-3.0", + "keywords": [ + "emailing", + "emailing management" + ], + "homepage": "https://github.com/sil-project/Emailing", + "authors": [ + { + "name": "Vivien FRASCA", + "email": "vivien.frasca@libre-informatique.fr" + }, + { + "name": "Libre Informatique", + "homepage": "http://www.libre-informatique.fr/" + } + ], + + "autoload": { + "psr-4": { + "Sil\\Component\\Emailing\\": "." + } + }, + "config": { + "bin-dir": "bin/" + }, + "version": "dev-wip-platform" +} diff --git a/src/Sil/Component/Emailing/etc/dockerfile.jenkins b/src/Sil/Component/Emailing/etc/dockerfile.jenkins new file mode 100644 index 00000000..5acb9820 --- /dev/null +++ b/src/Sil/Component/Emailing/etc/dockerfile.jenkins @@ -0,0 +1,73 @@ +FROM debian:latest +# https://issues.jenkins-ci.org/browse/JENKINS-44609 +# https://issues.jenkins-ci.org/browse/JENKINS-31507 +#as phpcli_for_platform + +RUN apt-get update + +RUN apt-get install -y apt-transport-https lsb-release ca-certificates git curl wget build-essential g++ unzip net-tools lsof dnsutils vim python-pip + +RUN echo "deb http://ftp.debian.org/debian $(lsb_release -sc)-backports main" >> /etc/apt/sources.list && apt-get update +RUN wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg && echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list && apt-get update + + +#RUN apt-get install -y libpq-dev libcurl4-openssl-dev libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng12-dev zlib1g-dev libicu-dev unixodbc-dev libxml2-dev libaio-dev libmemcached-dev freetds-dev + +RUN apt-get install -y nodejs python python3 +RUN apt-get install -y openjdk-8-jdk openjdk-8-jre && update-alternatives --config java +RUN apt-get install -y xvfb chromium chromium-l10n firefox-esr postgresql-client + + +ARG PHPVER=7.1 +RUN apt-get install -y php${PHPVER} php${PHPVER}-cli php${PHPVER}-pgsql php${PHPVER}-mysql php${PHPVER}-curl php${PHPVER}-json php${PHPVER}-gd php${PHPVER}-mcrypt php${PHPVER}-intl php${PHPVER}-sqlite3 php${PHPVER}-gmp php${PHPVER}-geoip php${PHPVER}-mbstring php${PHPVER}-redis php${PHPVER}-xml php${PHPVER}-zip php${PHPVER}-xdebug + +RUN echo "phar.readonly = Off" >> /etc/php/${PHPVER}/cli/conf.d/42-phar-readonly.ini +RUN echo "memory_limit=-1" >> /etc/php/${PHPVER}/cli/conf.d/42-memory-limit.ini + +RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer && chmod 755 /usr/local/bin/composer + +ENV HOME=/home/jenkins +ENV USER=jenkins +ENV GROUP=users + + + +# -u $(id -u) +# because jenkins docker pipeline force a -u option on run +# https://github.com/jenkinsci/docker-workflow-plugin/pull/25/files +# see whoAmI function here: +# https://github.com/ndeloof/docker-workflow-plugin/blob/master/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java +# it should be fixed in next plugins version >= 1.14 + +# jenkins user on sandboxrd has the 1004 id and it is forced by pipeline plugin on docker log on + + +ARG UID=1004 +RUN useradd -d $HOME -g $GROUP -u ${UID} -m $USER + + +# --no-create-home +# if we don't want skeleton file so we create home by hand (or by RUN :) ) +# RUN mkdir -p $HOME && mkdir -p $HOME/bin && chown -R $USER:$GROUP $HOME + +RUN mkdir -p $HOME/bin && chown -R $USER:$GROUP $HOME + +USER $USER:$GROUP +WORKDIR $HOME + +#should not be good, where does the current $PATH came from ? +ENV PATH=$PATH:$HOME/bin:$HOME/.local/bin/ + +# Add some before install step to run test faster +RUN composer self-update --no-progress --stable && composer global config bin-dir ${HOME}/bin +RUN pip install --upgrade pip + +RUN for i in "sllh/composer-lint" "pdepend/pdepend" "squizlabs/php_codesniffer" "phploc/phploc" "phpmd/phpmd" "sebastian/phpcpd" "theseer/phpdox" "phpmetrics/phpmetrics"; do composer global require $i:@stable --prefer-dist --no-interaction; done + +ENV NVM_DIR="$HOME/.nvm" +RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm install 8.9 && npm config set color false + +#Try to cache various tools +RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz && gunzip -f geckodriver-v0.19.1-linux64.tar.gz && tar -xvf geckodriver-v0.19.1-linux64.tar && mv geckodriver ${HOME}/bin/ +RUN wget -q https://selenium-release.storage.googleapis.com/3.7/selenium-server-standalone-3.7.0.jar && mv selenium-server-standalone-3.7.0.jar ${HOME}/bin/selenium-server-standalone.jar +RUN wget -q https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar --output-document="${HOME}/bin/coveralls" && chmod u+x "${HOME}/bin/coveralls" diff --git a/src/Sil/Component/Emailing/etc/pipeline.jenkins b/src/Sil/Component/Emailing/etc/pipeline.jenkins new file mode 100644 index 00000000..6e39fe76 --- /dev/null +++ b/src/Sil/Component/Emailing/etc/pipeline.jenkins @@ -0,0 +1,153 @@ +pipeline { + agent { + dockerfile { + filename "etc/dockerfile.jenkins" + args '--network=ci.network --volume /home/jenkins/cache/composer:/home/jenkins/.composer/cache' + } + } + + environment { + RND = "${BUILD_NUMBER}_${BRANCH_NAME}" + } + + options { + timeout(time: 1, unit: 'HOURS') + timestamps() + disableConcurrentBuilds() + } + + stages { + stage ('Where Am I') { + steps { + sh "uname -a" + sh "php -v" + sh "composer -V" + } + } + stage ('Set Env') { + steps { + sh "ln -fs ./.env.jenkins ./.env" + sh "cat ./.env" + sh "mkdir -p build" + } + } + + + stage ('Prepare') { + steps { + sh "./bin/ci-scripts/do_run.sh before_install_test.sh" + } + } + + stage ('Create Database') { + steps { + sh "./bin/ci-scripts/do_run.sh set_db_host_test.sh # needed before create as it set .pgpass" + sh "./bin/ci-scripts/do_run.sh create_database_test.sh" + sh "./bin/ci-scripts/do_run.sh create_table_test.sh" + + } + } + + stage('Parallel Install') { + parallel { + stage ('Install Test') { + steps { + sh "./bin/ci-scripts/do_run.sh install_test.sh" + } + } + + stage ('Install Doc') { + steps { + sh "./bin/ci-scripts/do_run.sh install_doc.sh" + } + } + } + } + stage('Parallel Start') { + parallel { + stage ('Start Project') { + steps { + sh "./bin/ci-scripts/do_run.sh before_script_test.sh" + } + } + + stage ('Start Selenium') { + steps { + sh "./bin/ci-scripts/do_run.sh launch_selenium_test.sh" + } + } + } + } + + stage('Parallel Report') { + parallel { + stage ('Run Test') { + steps { + sh "./bin/ci-scripts/do_run.sh run_test.sh" + step([ + $class: 'XUnitBuilder', + thresholds: [[$class: 'FailedThreshold', unstableThreshold: '1']], + tools: [[$class: 'JUnitType', pattern: 'build/junit.xml']] + ]) + publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'build/coverage', reportFiles: 'index.html', reportName: 'Coverage Report', reportTitles: '']) + } + } + + stage ('Run Doc') { + steps { + sh "./bin/ci-scripts/do_run.sh run_doc.sh" + } + } + + + stage('Check Style') { + steps { + sh 'phpcs -q --report=checkstyle --report-file=build/checkstyle.xml --standard=PSR2 --extensions=php --ignore=vendor ./ || exit 0' + checkstyle pattern: 'build/checkstyle.xml' + } + } + + stage('Copy Paste Detection') { + steps { + sh 'phpcpd -q --exclude=vendor --log-pmd build/pmd-cpd.xml ./ || exit 0' + dry canRunOnFailed: true, pattern: 'build/pmd-cpd.xml' + } + } + + stage('Mess Detection') { + steps { + sh 'phpmd ./ xml phpmd.xml.dist --exclude vendor --reportfile build/pmd.xml || exit 0' + pmd canRunOnFailed: true, pattern: 'build/pmd.xml' + } + } + + stage('Collect Metrics') { + steps { + sh "phpmetrics --quiet --excluded-dirs=vendor --report-html=build/metrics.html ./" + publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'build/', reportFiles: 'metrics.html', reportName: 'Metrics Report', reportTitles: '']) + } + } + } + } + /* + stage ('Archive Gzip') { + steps { + sh 'tar -czf Platform_${BRANCH_NAME}.tgz ./*' + archiveArtifacts artifacts: "Platform_${BRANCH_NAME}.tgz", fingerprint: true + } + } + */ + + + } + + + + post { + always { + cleanWs() + + } + } + +} diff --git a/src/Sil/Component/Emailing/phpmd.xml.dist b/src/Sil/Component/Emailing/phpmd.xml.dist new file mode 100644 index 00000000..766e122e --- /dev/null +++ b/src/Sil/Component/Emailing/phpmd.xml.dist @@ -0,0 +1,15 @@ + + Standard Sil Coding Standard + + + + + + + + \ No newline at end of file diff --git a/src/Sil/Component/Emailing/phpunit.xml.dist b/src/Sil/Component/Emailing/phpunit.xml.dist new file mode 100644 index 00000000..231b362a --- /dev/null +++ b/src/Sil/Component/Emailing/phpunit.xml.dist @@ -0,0 +1,50 @@ + + + + + + + + ./Tests/Unit + + + + + + + ./ + + ./Tests/ + ./Resources/ + ./vendor/ + ./coverage/ + + + + + + + + + + + + + + + +