diff --git a/_authors/vcomposieux.md b/_authors/vcomposieux.md new file mode 100644 index 000000000..b74d26855 --- /dev/null +++ b/_authors/vcomposieux.md @@ -0,0 +1,6 @@ +--- +layout: author +login: vcomposieux +name: Vincent Composieux +--- +Architecte passionné par les technologies web depuis de longues années, je pratique principalement du PHP (Symfony) / Javascript mais aussi du Python ou Golang. diff --git a/_drafts/2016-07-19-behat-structure-functional-tests.html b/_drafts/2016-07-19-behat-structure-functional-tests.html deleted file mode 100644 index 843132f2b..000000000 --- a/_drafts/2016-07-19-behat-structure-functional-tests.html +++ /dev/null @@ -1,293 +0,0 @@ ---- -layout: post -title: 'Behat: structure your functional tests' -author: vcomposieux -date: '2016-07-19 14:15:31 +0200' -date_gmt: '2016-07-19 12:15:31 +0200' -categories: -- Non classé -tags: [] ---- -{% raw %} -
Introduction
-In order to ensure that your application is running well, it's important to write functional tests.
-Behat is the most used tool with Symfony to handle your functional tests and that's great because it's really a complete suite.
-You should nevertheless know how to use it wisely in order to cover useful and complete test cases and that's the goal of this blog post.
-
Functional testing: what's that?
-When we are talking about functional testing we often mean that we want to automatize human-testing scenarios over the application.
-However, it is important to write the following test types to cover the functional scope:
-Idea is to develop and run both integration tests and interface tests with Behat.
-Before we can go, please note that we will use a Selenium server which will receive orders by Mink (a Behat extension) and will pilot our browser (Chrome in our configuration).
-To be clear on the architecture we will use, here is a scheme that will resume the role of all elements:
-[caption id="attachment_1997" align="alignnone" width="781"] Behat architecture scheme[/caption]
--
Behat set up
-First step is to install Behat and its extensions as dependencies in our composer.json file:
-"require-dev": { - "behat/behat": "~3.1", - "behat/symfony2-extension": "~2.1", - "behat/mink": "~1.7", - "behat/mink-extension": "~2.2", - "behat/mink-selenium2-driver": "~1.3", - "emuse/behat-html-formatter": "dev-master" -}-
In order to make your future contexts autoloaded, you also have to add this little PSR-4 section:
-"autoload-dev": { - "psr-4": { - "Acme\Tests\Behat\Context\": "features/context/" - } -}-
Now, let's create our behat.yml file in our project root directory in order to define our tests execution.
-Here is the configuration file we will start with:
-default: - suites: ~ - extensions: - Behat\Symfony2Extension: ~ - Behat\MinkExtension: - base_url: "http://acme.tld/" - selenium2: - browser: chrome - wd_host: 'http://selenium-host:4444/wd/hub' - default_session: selenium2 - emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension: - name: html - renderer: Twig,Behat2 - file_name: index - print_args: true - print_outp: true - loop_break: true - formatters: - pretty: ~ - html: - output_path: %paths.base%/web/reports/behat-
We will talk of all of these sections in their defined order so let's start with the suites section which is empty at this time but we will implement it later when we will have some contexts to add into it.
-Then, we load some Behat extensions:
-Finally, in the formatters section we keep the pretty formatter in order to keep an output in our terminal and the HTML reports will be generated at the same time in the web/reports/behat directory in order to make them available over HTTP (it should not be a problem as you should not execute functional tests in production, be careful to restrict access in this case).
-Now that Behat is ready and configured we will prepare our functional tests that we will split into two distinct Behat suites: integration and interface.
--
Writing functional tests (features)
-In our example, we will write tests in order to ensure that a new user can register over a registration page.
-We will have to start by writing our tests scenarios (in a .feature file) that we will put into a features/ directory located at the project root directory.
-So for instance, we will have the following scenario:
-File: features/registration/register.feature:
--
Feature: Register - In order to create an account - As a user - I want to be able to register on the application - -Scenario: I register when I fill my username and password only - Given I am on the registration page - And I register with username "johndoe" and password "azerty123" - When I submit the form - Then I should see the registration confirmation-
-
Integration tests
-As said previously, these tests are here to ensure all code written for the registration page can be executed and linked without any errors.
-To do so, we will create a new integration context that concerns the registration part under directory features/context/registration:
-File: features/context/registration/IntegrationRegisterContext:
-<?php - -namespace Acme\Tests\Behat\Context\Registration; - -use Acme\AppBundle\Entity\User; -use Acme\AppBundle\Registration\Registerer; -use Behat\Behat\Context\Context; - -/** - * Integration register context. - */ -class IntegrationRegisterContext implements Context -{ - /** - * Registerer - */ - protected $registerer; - - /** - * User - */ - protected $user; - - /** - * boolean - */ - protected $response; - - /** - * Constructor. - * - * @param Registerer $registerer - */ - public function __construct(Registerer $registerer) - { - $this->registerer = $registerer; - } - - /** - * @Given I am on the registration page - */ - public function iAmOnTheRegistrationPage() - { - $this->user = new User(); - } - - /** - * @Given /I register with username "(?P<username>[^"]*)" and password "(?P<password>[^"]*)"/ - */ - public function iRegisterWithUsernameAndPassword($username, $password) - { - $this->user->setUsername($username); - $this->user->setPassword($password); - } - - /** - * @When I submit the form - */ - public function iSubmitTheForm() - { - $this->response = $this->registerer->register($this->user); - } - - /** - * @Then I should see the registration confirmation message - */ - public function iShouldSeeTheRegistrationConfirmation() - { - if (!$this->response) { - throw new \RuntimeException('User is not registered.'); - } - } -}-
Integration test for this part is now done for our feature. Let's write the interface test now!
--
Interface tests
-This test will be based on the same feature file without modifying the original written scenarios we wrote at the beginning. That's why it is important to write a generic test that can be implemented both in an integration test and in an interface test.
-So let's create that context that will be used for interface test (prefixed by Mink in our case, but you can prefix it by anything you want) under the directory features/context/registration.
-File: features/context/registration/MinkRegisterContext:
--
<?php - -namespace Acme\Tests\Behat\Context\Registration; - -use Acme\AppBundle\Entity\User; -use Acme\AppBundle\Registration\Registerer; -use Behat\Behat\Context\Context; -use Behat\MinkExtension\Context\MinkContext; - -/** - * Mink register context. - */ -class MinkRegisterContext extends MinkContext -{ - /** - * @Given I am on the registration page - */ - public function iAmOnTheRegistrationPage() - { - $this->visit('/register'); - } - - /** - * @Given /I register with username "(?P<username>[^"]*)" and password "(?P<password>[^"]*)"/ - */ - public function iRegisterWithUsernameAndPassword($username, $password) - { - $this->fillField('registration[username]', $username); - $this->fillField('registration[password]', $password); - } - - /** - * @When I submit the form - */ - public function iSubmitTheForm() - { - $this->pressButton('Register'); - } - - /** - * @Then I should see the registration confirmation message - */ - public function iShouldSeeTheRegistrationConfirmation() - { - $this->assertPageContainsText('Congratulations, you are now registered!'); - } -}-
We just implemented an interface test based on the same scenario that the one we used for integration test so this class has exactly the same four methods with the same Behat annotations that we have implemented in our integration test class.
-The only difference here is that in this context we ask Mink to ask to Selenium to do actions on the interface of our application by executing a browser instead of testing the code itself.
--
Context definition
-One more thing now, we have to add previously created contexts in our suites section in the behat.yml configuration file.
-suites: - integration: - paths: - - %paths.base%/features/registration - contexts: - - Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext: - - "@acme.registration.registerer" - interface: - paths: - - %paths.base%/features/registration - contexts: - - Behat\MinkExtension\Context\MinkContext: [] - - Acme\Tests\Behat\Context\Registration\MinkRegisterContext: []-
It is important to see here that we can clearly split these kind of tests into two distinct parts integration and interface: each one will be executed with its own contexts.
-Also, as we have loaded the Symfony2 extension during the Behat set up, we have the possibility to inject Symfony services in our contexts and that case occurs here with the acme.registration.registerer service.
--
Tests execution
-In order to run all tests, simply execute in the project root directory: bin/behat -c behat.yml.
-If you want to run the integration tests only: bin/behat -c behat.yml --suite=integration.
-HTML report will be generated under the web/reports/behat/ as specified in the configuration that will allow you to have a quick overview of failed tests which is cool when you have a lot of tests.
--
Link multiple contexts together
-At last, sometime you could need information from another context. For instance, imagine that you have a second step just after the register step. You will have to create two new IntegrationProfileContext and MinkProfileContext contexts.
-We will only talk about integration context in the following to simplify understanding.
-In this new step IntegrationProfileContext, you need some information obtained in the first step IntegrationRegisterContext.
This can be achieved thanks to the @BeforeScenario Behat annotation.
-File: features/context/registration/IntegrationProfileContext:
--
<?php - -namespace Acme\Tests\Behat\Context\Registration; - -use Behat\Behat\Context\Context; -use Behat\Behat\Hook\Scope\BeforeScenarioScope; - -/** - * Integration registration profile context. - */ -class IntegrationProfileContext implements Context -{ - /** - * IntegrationRegisterContext - */ - protected $registerContext; - - /** - * @BeforeScenario - */ - public function gatherContexts(BeforeScenarioScope $scope) - { - $environment = $scope->getEnvironment(); - - $this->registerContext = $environment->getContext( - 'Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext' - ); - } -}-
You now have an accessible property $registerContext and can access informations from this context.
--
Conclusion
-Everything starts from well-written tests which have to be thoughtful in order to allow a technical implementation on both integration tests and interface tests.
-The choosen structure about classifying its tests is also important in order to quickly find tests when the application grows.
Introduction
-Il est important de mettre en place des tests fonctionnels sur les projets afin de s'assurer du bon fonctionnement de l'application.
-Lorsqu'il s'agit d'une application Symfony, Behat est l'outil le plus souvent utilisé pour réaliser ces tests et c'est tant mieux car cet outil est très complet.
-Il faut néanmoins savoir l'utiliser à bon escient afin de couvrir des cas de tests utiles et complets, c'est ce que nous allons voir dans cet article.
-
Tests fonctionnels : qu'est-ce ?
-Lorsque nous parlons de "tests fonctionnels", nous entendons bien souvent vouloir tester l'interface de l'application (site web), autrement dit, automatiser des tests qui pourraient être faits par un humain.
-Or, il est important d'écrire les cas de tests suivants afin de couvrir le périmètre fonctionnel :
-Il conviendra alors de lancer à la fois les tests d'intégration et les tests d'interface avec Behat.
-Avant de commencer, notez que dans cet exemple, nous allons utiliser un serveur Selenium qui recevra les informations fournies par Mink (extension de Behat) et qui pilotera ensuite notre navigateur (Chrome, dans notre configuration).
-Pour être clair sur l'architecture, voici un schéma qui résume le rôle de chacun :
-[caption id="attachment_1986" align="alignnone" width="781"] Schéma d'architecture Behat/Selenium[/caption]
--
Mise en place de Behat
-La première étape est d'installer Behat et ses extensions en tant que dépendance dans notre fichier composer.json :
-"require-dev": { - "behat/behat": "~3.1", - "behat/symfony2-extension": "~2.1", - "behat/mink": "~1.7", - "behat/mink-extension": "~2.2", - "behat/mink-selenium2-driver": "~1.3", - "emuse/behat-html-formatter": "dev-master" -}-
Afin que vos futurs contextes soient autoloadés, nous allons également ajouter la section PSR-4 suivante :
-"autoload-dev": { - "psr-4": { - "Acme\Tests\Behat\Context\": "features/context/" - } -}-
Maintenant, créons le fichier de configuration behat.yml à la racine de notre projet afin d'architecturer nos tests.
-Voici le fichier de configuration à partir duquel nous allons débuter :
-default: - suites: ~ - extensions: - Behat\Symfony2Extension: ~ - Behat\MinkExtension: - base_url: "http://acme.tld/" - selenium2: - browser: chrome - wd_host: 'http://selenium-host:4444/wd/hub' - default_session: selenium2 - emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension: - name: html - renderer: Twig,Behat2 - file_name: index - print_args: true - print_outp: true - loop_break: true - formatters: - pretty: ~ - html: - output_path: %paths.base%/web/reports/behat-
Si nous prenons les sections dans leur ordre, nous avons avant tout une section suites pour le moment vide mais que nous allons alimenter par la suite de cet article.
-Ensuite, nous chargeons ici plusieurs extensions de Behat :
-Notons enfin que dans la section formatters, nous conservons le formatter pretty afin d'avoir une sortie sympa sur notre terminal et que les rapports HTML seront quant à eux générés dans le répertoire web/reports/behat afin qu'ils soient accessibles en HTTP (à priori pas de soucis car vous ne devriez pas jouer ces tests en production, attention à la restriction d'accès si c'est le cas).
-Maintenant que Behat est prêt et configuré, nous allons préparer nos tests fonctionnels que nous allons découper en deux "suites" Behat distinctes : integration et interface.
--
Ecriture des tests fonctionnels (features)
-Nous allons partir sur des tests permettant de s'assurer du bon fonctionnement d'une page d'inscription.
-Nous devons avant tout écrire nos scénarios de tests fonctionnels (fichier .feature) que nous allons placer dans un répertoire features/ à la racine du projet.
-Nous allons donc avoir, par exemple, le scénario suivant :
-Fichier : features/registration/register.feature :
-Feature: Register - In order to create an account - As a user - I want to be able to register on the application - -Scenario: I register when I fill my username and password only - Given I am on the registration page - And I register with username "johndoe" and password "azerty123" - When I submit the form - Then I should see the registration confirmation-
-
Tests d'intégration
-Il va maintenant convenir d'implémenter le code qui va nous permettre de tester que le code écrit pour l'inscription d'un utilisateur peut être exécuté et enchaîné sans erreur.
-Nous allons donc créer un contexte d'intégration propre à l'inscription sous le répertoire features/context/registration :
-Fichier : features/context/registration/IntegrationRegisterContext :
-<?php - -namespace Acme\Tests\Behat\Context\Registration; - -use Acme\AppBundle\Entity\User; -use Acme\AppBundle\Registration\Registerer; -use Behat\Behat\Context\Context; - -/** - * Integration register context. - */ -class IntegrationRegisterContext implements Context -{ - /** - * Registerer - */ - protected $registerer; - - /** - * User - */ - protected $user; - - /** - * boolean - */ - protected $response; - - /** - * Constructor. - * - * @param Registerer $registerer - */ - public function __construct(Registerer $registerer) - { - $this->registerer = $registerer; - } - - /** - * @Given I am on the registration page - */ - public function iAmOnTheRegistrationPage() - { - $this->user = new User(); - } - - /** - * @Given /I register with username "(?P<username>[^"]*)" and password "(?P<password>[^"]*)"/ - */ - public function iRegisterWithUsernameAndPassword($username, $password) - { - $this->user->setUsername($username); - $this->user->setPassword($password); - } - - /** - * @When I submit the form - */ - public function iSubmitTheForm() - { - $this->response = $this->registerer->register($this->user); - } - - /** - * @Then I should see the registration confirmation message - */ - public function iShouldSeeTheRegistrationConfirmation() - { - if (!$this->response) { - throw new \RuntimeException('User is not registered.'); - } - } -}-
L'implémentation du test d'intégration est terminé pour cette feature !
-Passons maintenant au test d'interface !
-
Tests d'interface
-Ce test va se baser sur la même feature et nous n'avons absolument rien modifié dans le test précédemment écrit. C'est pourquoi il est important de bien rédiger ses tests fonctionnels afin qu'ils restent assez génériques pour être implémentés à la fois en test d'intégration et en test d'interface.
-Créons donc le contexte qui sera utilisé pour le test d'interface (préfixé par Mink dans notre cas, mais vous pouvez préfixer par ce que vous voulez) sous le même répertoire features/context/registration :
-Fichier : features/context/registration/MinkRegisterContext :
-<?php - -namespace Acme\Tests\Behat\Context\Registration; - -use Acme\AppBundle\Entity\User; -use Acme\AppBundle\Registration\Registerer; -use Behat\Behat\Context\Context; -use Behat\MinkExtension\Context\MinkContext; - -/** - * Mink register context. - */ -class MinkRegisterContext extends MinkContext -{ - /** - * @Given I am on the registration page - */ - public function iAmOnTheRegistrationPage() - { - $this->visit('/register'); - } - - /** - * @Given /I register with username "(?P<username>[^"]*)" and password "(?P<password>[^"]*)"/ - */ - public function iRegisterWithUsernameAndPassword($username, $password) - { - $this->fillField('registration[username]', $username); - $this->fillField('registration[password]', $password); - } - - /** - * @When I submit the form - */ - public function iSubmitTheForm() - { - $this->pressButton('Register'); - } - - /** - * @Then I should see the registration confirmation message - */ - public function iShouldSeeTheRegistrationConfirmation() - { - $this->assertPageContainsText('Congratulations, you are now registered!'); - } -}-
Nous venons d'implémenter un test d'interface basé sur le même scénario que celui que nous avons utilisé pour notre test d'intégration, reprenant exactement les quatre méthodes implémentées précédemment avec les mêmes annotations Behat.
-La seule différence est que dans ce contexte, Mink va demander à Selenium d'effectuer les actions au niveau de l'interface de notre application en pilotant un navigateur au lieu de tester le code lui-même.
--
Définitions des contextes
-Il ne nous reste plus qu'à ajouter les contextes créés précédemment sous notre section suites dans le fichier de configuration behat.yml :
-suites: - integration: - paths: - - %paths.base%/features/registration - contexts: - - Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext: - - "@acme.registration.registerer" - interface: - paths: - - %paths.base%/features/registration - contexts: - - Behat\MinkExtension\Context\MinkContext: [] - - Acme\Tests\Behat\Context\Registration\MinkRegisterContext: []-
Il est important de voir ici que nous découpons clairement les tests en deux suites distinctes : integration et interface : chacune d'entre elles sera exécutée avec les contextes qui lui sont propres.
-Etant donné que nous avons chargés l'extension Symfony2 lors de la mise en place de Behat, nous avons la possibilité d'injecter des services Symfony dans nos contextes, c'est le cas ici avec le service acme.registration.registerer.
--
Exécution des tests
-Pour lancer tous les tests, exécutez simplement, à la racine du projet : bin/behat -c behat.yml.
-Pour lancer uniquement la suite d'integration, par exemple : bin/behat -c behat.yml --suite=integration.
-Le rapport HTML est quand à lui généré dans web/reports/behat/, comme spécifié dans notre configuration, ce qui vous permettra d'avoir un aperçu rapide des tests qui échouent, plutôt pratique lorsque vous avez de nombreux tests.
--
Lier plusieurs contextes entre eux
-Pour terminer, vous pourrez parfois avoir besoin de lier les contextes entre eux. Par exemple, imaginons que vous ayez une deuxième page sur votre formulaire d'inscription pour renseigner les informations personnelles, vous allez alors créer deux nouveaux contextes IntegrationProfileContext et MinkProfileContext.
-Partons sur le contexte d'intégration pour simplifier l'explication, l'idée est de ne pas dupliquer le code précédemment créé et permettant de tester la première étape IntegrationRegisterContext et de réutiliser ces informations dans le nouveau contexte IntegrationProfileContext.
Ceci est possible grâce à l'annotation @BeforeScenario de Behat.
-Fichier : features/context/registration/IntegrationProfileContext :
-<?php - -namespace Acme\Tests\Behat\Context\Registration; - -use Behat\Behat\Context\Context; -use Behat\Behat\Hook\Scope\BeforeScenarioScope; - -/** - * Integration registration profile context. - */ -class IntegrationProfileContext implements Context -{ - /** - * IntegrationRegisterContext - */ - protected $registerContext; - - /** - * @BeforeScenario - */ - public function gatherContexts(BeforeScenarioScope $scope) - { - $environment = $scope->getEnvironment(); - - $this->registerContext = $environment->getContext( - 'Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext' - ); - } -}-
Vous avez maintenant à disposition une propriété $registerContext et pouvez accéder à des informations qui proviennent du contexte précédent.
--
Conclusion
-Tout part de l'écriture des tests fonctionnels qui doivent être bien réfléchis pour ensuite permettre une implémentation technique à la fois en test d'intégration mais aussi en test d'interface.
-La structure choisie pour classer ses tests fonctionnels est aussi importante pour pouvoir s'y retrouver rapidement dans les différents scénarios de test lorsque l'application prend de l'ampleur.
Depuis Symfony 3.2, un nouveau composant très utile a vu le jour : le composant Workflow.
-Celui-ci est en effet très pratique et peut très largement simplifier vos développements lorsque vous avez, par exemple, à gérer des workflows de statut dans votre application.
-
Installation
-Dans tous les cas, vous devez installer la dépendance suivante :
-"symfony/workflow": "~3.2@dev"-
Si vous utilisez une version antérieure de Symfony mais >=2.3, c'est aussi possible mais il vous faudra également installer ce bundle non-officiel qui embarque le composant et ajoute la configuration nécessaire sous le namespace du bundle :
-"fduch/workflow-bundle": "~0.2@dev"-
Pensez bien à activer le bundle dans votre kernel.
--
Configuration
-Il va maintenant nous falloir définir la configuration de notre workflow et ainsi définir les statuts (appelés places) et transitions possibles.
-Pour cet article, nous sommes partis sur un exemple basé sur les statuts d'une pull request. Celle-ci peut avoir les états suivants : opened , closed , needs_review , reviewed et enfin merged .
-Cependant, elle ne pourra, par exemple, pas être passée en merged sans être passée par le statut reviewed . C'est ici que le composant Workflow prend tout son sens.
--
Voici ce que donne notre configuration complète :
-workflow: - workflows: - pull_request: - marking_store: - type: multiple_state - arguments: - - state - supports: - - AppBundle\Entity\PullRequest - places: - - opened - - closed - - needs_review - - reviewed - - merged - transitions: - feedback: - from: opened - to: needs_review - review: - from: [opened, needs_review] - to: reviewed - merge: - from: reviewed - to: merged - close: - from: [opened, needs_review, reviewed] - to: closed-
Nous spécifions ici que nous souhaitons utiliser un workflow de type multiple_state . Notez que si vous souhaitez utiliser une transition simple d'un statut vers un autre, vous pouvez utiliser ici single_state.
-Nous disposons donc également d'une classe AppBundle\Entity\PullRequest qui dispose d'une propriété state ainsi que son setter et getter associé (le composant va utiliser les méthodes getter et setter pour changer l'état et/ou obtenir l'état courant) :
-<?php - -namespace AppBundle\Entity; - -use Doctrine\ORM\Mapping as ORM; - -/** - * @ORM\Table(name="pull_request") - */ -class PullRequest -{ - /** - * @ORM\Column(type="json_array", nullable=true) - */ - protected $state; - - public function setState($state) - { - $this->state = $state; - } - - public function getState() - { - return $this->state; - } -}-
-
Nous avons terminé, nous pouvons maintenant commencer à utiliser le composant Workflow !
--
Utilisation
-La première chose utile à effectuer après avoir écrit votre workflow est de générer une représentation graphique de celui-ci (sous un format Graphviz).
-Pour se faire, nous utilisons la commande Symfony :
-$ bin/console workflow:dump pull_request-
-
Celle-ci va vous générer un code Graphviz qui donne le schéma suivant :
- -Celui-ci permet vraiment de donner une vision claire de son workflow, à tous les niveaux (développeurs, product owners, clients, ...).
-Le composant Workflow implémente des méthodes permettant d'effectuer une transition, vérifier si une transition peut être effectuée avec l'état actuel et lister les transitions possibles avec l'état actuel.
--
Pour vérifier si vous pouvez effectuer une transition et l'appliquer, rien de plus simple :
-<?php - -namespace AppBundle\Controller; - -use AppBundle\Manager\PullRequestManager; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\HttpFoundation\RedirectResponse; - -class PullRequestController extends Controller -{ - /** - * @param int $identifier A pull request identifier. - * - * @return RedirectResponse - */ - public function update($identifier) - { - ... - - // Notre pull request est au statut "reviewed" - $pullRequest = $this->getPullRequestManager()->find($identifier); - - // Nous obtenons le service "workflow.<nom du workflow>" - $workflow = $this->get('workflow.pull_request'); - - if ($workflow->can($pullRequest, 'merge')) { - $workflow->apply($pullRequest, 'merge'); - } - - ... - } -}-
Si vous ne passez pas par la méthode can() , la méthode apply() renverra une exception si la transition ne peut pas être effectuée. Vous pouvez donc également catcher cette exception de type Symfony\Component\Workflow\Exception\LogicException .
--
Pour lister les transitions disponibles :
-$workflow->getEnabledTransitions($pullRequest);-
Globalement, l'utilisation du composant se limite à ces 3 méthodes. Comme vous le remarquez, il devient très simple d'utiliser un workflow, même complexe !
--
Branchez-vous sur les événements !
-Le composant utilise également plusieurs événements, à savoir, dans l'ordre chronologique :
-Enfin, sachez que ces événements existent aussi en version unique pour chaque workflow afin de vous permettre de vous brancher dessus uniquement sur certains workflows. Il vous faut alors utiliser le nom workflow.pull_request.enter.
-Faisons encore mieux, vous pouvez même vous brancher sur une transition particulière :
--
Conclusion
-Le composant Workflow est vraiment très utile dans la gestion d'états ou de statuts sur la plupart des projets.
-N'hésitez pas à l'utiliser, sa facilité de configuration et d'utilisation vous aidera grandement sur vos projets.
-Aussi, il m'a permis de donner un graphique clair sur un workflow complexe à toutes les personnes avec qui je travaillais.
-{% endraw %} diff --git a/_drafts/2016-09-29-symfony-workflow-component.html b/_drafts/2016-09-29-symfony-workflow-component.html deleted file mode 100644 index bbb432a89..000000000 --- a/_drafts/2016-09-29-symfony-workflow-component.html +++ /dev/null @@ -1,160 +0,0 @@ ---- -layout: post -title: Use the Symfony Workflow component -author: vcomposieux -date: '2016-09-29 10:04:20 +0200' -date_gmt: '2016-09-29 08:04:20 +0200' -categories: -- Non classé -tags: [] ---- -{% raw %} -Since Symfony 3.2, a new useful component was born: the Workflow component.
-It is indeed really convenient and can simplify greatly your developments when you have to manage status workflows in your application, that occurs a lot.
--
Installation
-In all cases, you have to install the following dependency:
-"symfony/workflow": "~3.2@dev"-
If you use an earlier version of Symfony but >=2.3 you are also able to use this component, but you have to install the following non-official bundle, which loads the component itself and add the required configuration under the bundle's namespace:
-"fduch/workflow-bundle": "~0.2@dev"-
Do not forget to enable the bundle in your kernel class.
--
Configuration
-Time has come to write our workflow configuration. We will have to define all our places (statuses / states) and available transitions.
-In this blog post, we will take a pull request status example. A pull request can have one of the following status: opened , closed , needs_review , reviewed or merged.
-However, it cannot be, for instance, moved from the merged status without having the reviewed status before. The workflow component makes sense here.
--
Here is our full workflow configuration:
-workflow: - workflows: - pull_request: - marking_store: - type: multiple_state - arguments: - - state - supports: - - AppBundle\Entity\PullRequest - places: - - opened - - closed - - needs_review - - reviewed - - merged - transitions: - feedback: - from: opened - to: needs_review - review: - from: [opened, needs_review] - to: reviewed - merge: - from: reviewed - to: merged - close: - from: [opened, needs_review, reviewed] - to: closed-
Here, we specify we want to use a multiple_state workflow. Please not that if you want to use a simple transition from one state to another, you can use a single_state .
-For this example, we also have defined a AppBundle\Entity\PullRequest class which has a state property and associated setter and getter methods (component will use these methods to manage transitions):
-<?php - -namespace AppBundle\Entity; - -use Doctrine\ORM\Mapping as ORM; - -/** - * @ORM\Table(name="pull_request") - */ -class PullRequest -{ - /** - * @ORM\Column(type="json_array", nullable=true) - */ - protected $state; - - public function setState($state) - { - $this->state = $state; - } - - public function getState() - { - return $this->state; - } -}-
-
-
Everything is now ready, we can start to use the Workflow component!
--
Usage
-First useful thing to do after you have written your workflow configuration is to generate a graph using the Symfony command. The command will generate one graph using the Graphviz format.
--
Here is the Symfony command you have to run:
-$ bin/console workflow:dump pull_request-
The generated Graphviz will give you the following diagram:
- -This one gives you a really clear vision of your workflow and allows everyone at every level (developers, product owners, customers, ...) to understand the business logic.
-The Workflow component implements methods that allow you to verify if a transition is applicable and to later apply it depending on the current status and to also list all enabled transitions.
--
In order to check if you can apply a specific transition and apply it, simply use the following code:
-<?php - -namespace AppBundle\Controller; - -use AppBundle\Manager\PullRequestManager; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\HttpFoundation\RedirectResponse; - -class PullRequestController extends Controller -{ - /** - * @param int $identifier A pull request identifier. - * - * @return RedirectResponse - */ - public function update($identifier) - { - ... - - // Notre pull request est au statut "reviewed" - $pullRequest = $this->getPullRequestManager()->find($identifier); - - // Nous obtenons le service "workflow.<nom du workflow>" - $workflow = $this->get('workflow.pull_request'); - - if ($workflow->can($pullRequest, 'merge')) { - $workflow->apply($pullRequest, 'merge'); - } - - ... - } -}-
In the case you do not want to use the can() method, the apply() one will throw an exception if the transition cannot be effectively done, so you will be able to catch exceptions on the Symfony\Component\Workflow\Exception\LogicException type.
--
-
To list all enabled transitions:
-$workflow->getEnabledTransitions($pullRequest);-
Overall, the component usage is just as simple as these 3 methods. As you can see, complex workflows are now easier to manage!
--
Tune in for events!
-The component also dispatches multiple events, chronologically sorted as:
--
Finally, you have to know that these events also exist in a unique way for each workflow in order to allow you to tune in your workflow events only.
-If you want to do that, you have to listen to the following name: workflow.pull_request.enter .
-
Let's do better than that: you are also able to listen to a specific transition or a state for a specific workflow:
--
Conclusion
-The Workflow component is a really useful component to manage state or status on most of web applications.
-Do not hesitate to use it because its simplicity of configuration and use will probably help you a lot on your projects.
-Also, this component helps me a lot to give people I was working with a clear vision on a complex workflow we have to manage. The graph generation allows to clarify all of that for everyone!
-{% endraw %} diff --git a/_drafts/2016-12-01-creer-votre-premier-package-pour-atom.html b/_drafts/2016-12-01-creer-votre-premier-package-pour-atom.html deleted file mode 100644 index 2b2e33959..000000000 --- a/_drafts/2016-12-01-creer-votre-premier-package-pour-atom.html +++ /dev/null @@ -1,286 +0,0 @@ ---- -layout: post -title: Créer votre premier package pour Atom -author: vcomposieux -date: '2016-12-01 12:14:17 +0100' -date_gmt: '2016-12-01 11:14:17 +0100' -categories: -- Javascript -tags: -- atom -- package -- babel -- jasmine ---- -{% raw %} -Atom est un éditeur de texte (principalement utilisé pour du code) multi-plateforme développé par la société GitHub et qui s'appuie sur un autre framework développé par GitHub : Electron, qui permet de développer des applications natives pour chaque système d'exploitation à partir de code Javascript.
-Le grand intérêt d'Atom est qu'il peut être étendu très facilement avec un peu de code Javascript et c'est ce que nous allons voir dans cet article. Ainsi, tout le monde peut écrire son "package" pour Atom.
-Aussi, sa communauté très active compte déjà un bon nombre de packages : 5 285 au moment où j'écris cet article.
-Vous pouvez les retrouver à l'URL suivante : https://atom.io/packages
Si toutefois vous ne trouvez pas votre bonheur dans les packages déjà proposés, vous pouvez alors écrire le votre et nous allons voir qu'il n'y a rien de compliqué.
--
Pour créer votre premier package, rassurez-vous, vous n'allez pas partir de rien. En effet, nous allons utiliser la commande fournie par le package Package Generator natif à Atom.
-Pour se faire, il vous suffira de naviguer dans : Packages -> Package Generator -> Generate Atom Package.
-[note]Lors de la génération, vous pouvez choisir le langage que vous souhaitez utiliser pour développer votre package, entre Javascript et Coffeescript. Cet article est rédigé en Javascript.[/note]
-Atom vous ouvrira alors une nouvelle fenêtre à l'intérieur de votre nouveau package, nommé my-package .
--
Nous allons maintenant détailler la structure par défaut du projet :
-├── CHANGELOG.md -├── LICENSE.md -├── README.md -├── keymaps -│ └── my-package.json <- Raccourcis clavier enregistrés par votre package -├── lib -│ ├── my-package-view.js -│ └── my-package.js <- Point d'entrée de votre package -├── menus -│ └── my-package.json <- Déclaration des menus que votre package ajoute dans Atom -├── package.json <- Fichier descriptif et de dépendances de votre package -├── spec <- Répertoire de tests (Jasmine) de votre package -│ ├── my-package-spec.js -│ └── my-package-view-spec.js -└── styles <- Feuilles de styles utilisées par votre package -└── my-package.less-
-
Le premier élément à renseigner est le fichier package.json qui doit contenir les informations relatives à votre package tel que son nom, sa version, license, mots clés pour trouver votre package et également ses librairies de dépendances.
-Notez également la présence dans ce fichier d'une section activationCommands qui vous permet de définir la commande exécutée lors de l'activation de votre package.
-Nous avons ensuite le fichier keymaps/my-package.json qui vous permet d'enregistrer des raccourcis clavier dans votre application, de façon très simple :
-{ - "atom-workspace": { - "ctrl-alt-p": "my-package:toggle" - } -}-
-
Passons maintenant au point d'entrée de votre package. Il s'agit de ce qui se trouve dans lib/my-package.js .
-Dans ce fichier est exporté un objet par défaut qui contient une propriété subscriptions et des méthodes activate() et deactivate() notamment.
-Lors de l'activation de notre package (dans la méthode activate() ), nous allons enregistrer dans notre propriété subscriptions un objet de type CompositeDisposable qui nous permettra d'ajouter et d'éventuellement plus tard, supprimer des commandes disponibles dans notre package :
-activate(state) { - this.subscriptions = new CompositeDisposable(); - this.subscriptions.add(atom.commands.add('atom-workspace', { - 'my-package:toggle': () => this.toggle() - })); -}-
-
Notre commande étant enregistrée, nous pouvons dès maintenant l'exécuter en ouvrant la palette de commande : My Package: Toggle .
-Celle-ci va exécuter le code contenu dans la méthode toggle() de votre classe, soit dans le package par défaut, afficher une petite fenêtre en haut de l'écran.
Vous pouvez ajouter autant de commandes que vous le souhaitez et surtout, découper votre code comme vous le sentez.
--
Vous avez la possibilité d'ajouter des paramètres à votre package et ceci est rendu possible grâce au composant Config.
-Il vous suffira d'ajouter une propriété config à votre classe en définissant un objet avec la définition de chaque élément que vous souhaitez voir apparaître dans vos paramètres :
-config: { - "gitlabUrl": { - "description": "If you rely on a private Gitlab server, please type your base URI here (default: https://gitlab.com).", - "type": "string", - "default": "https://gitlab.com" - } -}-
-
La configuration offre un grand nombre de valeurs disponibles (boolean , color , integer , string , ...) ce qui permet de laisser un grand nombre de choix à vos utilisateurs.
-Les paramètres de votre package apparaîtront alors pour votre package, sous Atom :
- -Vous pourrez alors, à tout moment dans votre code, obtenir dans votre package la valeur définie par l'utilisateur (ou la valeur par défaut fournie si aucune valeur n'a été renseignée) via :
-let gitlabUrl = atom.config.get('gitlabUrl');-
-
Vous pouvez maintenant commencer à développer votre package, nous allons donc parcourir les différents composants qui sont à votre disposition et que vous pourrez utiliser dans votre package !
--
TextEditor : Agissez sur l'éditeur de texte
-Avec le composant TextEditor , vous allez pouvoir insérer du texte dans votre éditeur, enregistrer le fichier, jouer sur l'historique des actions (aller en avant ou arrière), déplacer le curseur dans l'éditeur, copier/coller dans le presse-papier, jouer sur l'indentation, scroller, etc ...
-Quelques commandes en exemple, ici pour insérer du texte à une coordonnée donnée et enregistrer le fichier :
-editor.setCursorBufferPosition([row, column]); -editor.insertText('foo'); -editor.save();-
-
ViewRegistry et View : Créez et affichez votre propre fenêtre
-Ces composants vont vous permettre de créer votre fenêtre à l'intérieur d'Atom et de l'afficher.
-Vous disposez d'un exemple d'utilisation du composant View dans le package généré par défaut :
export default class MyPackageView { - constructor(serializedState) { - // Create root element - this.element = document.createElement('div'); - this.element.classList.add('my-package'); - - // Create message element - const message = document.createElement('div'); - message.textContent = 'The MyPackage package is Alive! It --
NotificationManager et Notification : Informez vos utilisateurs via des notifications
-Vous avez également la possibilité de rendre des notifications dans l'éditeur de plusieurs niveaux, avec les commandes suivantes :
-atom.notifications.addSuccess('My success notification'); -atom.notifications.addInfo('My info notification'); -atom.notifications.addWarning('My warning notification'); -atom.notifications.addError('My error notification'); -atom.notifications.addFatalError('My fatal error notification');--
GitRepository
-Celui-ci est très intéressant : vous pouvez en effet accéder à toutes les propriétés du repository Git actuellement utilisé par l'utilisateur.
-Vous pourrez alors obtenir (entre autres) la branche actuellement utilisée, obtenir l'URL du remote, voir si un fichier est nouveau ou modifié ou encore accéder au diff.
-let repository = atom.project.getRepositoryForDirectory('/path/to/project'); - -console.log(repository.getOriginURL()); // -> git@github.com:eko/atom-pull-request.git -console.log(repository.getShortHead()); // -> master -console.log(repository.isStatusNew('/path/to/file')); // -> true--
Encore bien d'autres choses à découvrir ...
-Je vous ai présenté les composants les plus courants mais je vous invite à visiter la documentation de l'API si vous souhaitez aller plus loin : https://atom.io/docs/api/latest/AtomEnvironment
--
Tester votre package
-Nous en arrivons au moment de tester notre package, et pour cela, Atom utilise Jasmine.
-Votre package vient déjà avec un fichier de test pré-défini :
-import MyPackageView from '../lib/my-package-view'; - -describe('MyPackageView', () => { - it('has one valid test', () => { - expect('life').toBe('easy'); - }); -});--
-
Les tests Jasmine doivent être structurés de la façon suivante :
-
C'est maintenant à vous de jouer et de tester votre logique applicative.
--
Vous pouvez lancer les specs via le menu d'Atom : View -> Packages -> Run Package Specs .
--
Notre package est maintenant prêt à être publié !
- -Pour se faire, nous allons utiliser l'outil CLI installé avec Atom : apm .
-Après avoir pushé votre code sur un repository Github, rendez-vous dans le répertoire de votre package et jouez la commande suivante :
$ apm publish --tag v0.0.1 minor - -Preparing and tagging a new version ✓ -Pushing v0.0.1 tag ✓ -...-
-
La commande va s'occuper de créer et pusher le tag de la version spécifiée et référencer cette version sur le registry d'Atom.
-Félicitations, votre package est maintenant publié et visible à l'URL : https://atom.io/packages/<votre-package> !
--
Afin de vous assurer que votre package fonctionne toujours sur la version stable d'Atom mais également pour anticiper et tester également la version bêta, vous pouvez mettre en place Travis-CI sur le repository de votre code avec le fichier suivant :
-language: objective-c - -notifications: - email: - on_success: never - on_failure: change - -script: 'curl -s https://raw.githubusercontent.com/nikhilkalige/docblockr/develop/spec/atom-build-package.sh | sh' - -env: - global: - - APM_TEST_PACKAGES="" - - matrix: - - ATOM_CHANNEL=stable - - ATOM_CHANNEL=beta-
-
Je trouve personnellement qu'il s'agit d'une vraie révolution de pouvoir interagir à tel point avec l'éditeur de texte, l'outil utilisé la plupart du temps par les développeurs.
-L'API d'Atom est déjà très riche et est très simple à utiliser, c'est très certainement la raison pour laquelle la communauté offre déjà un bon nombre de packages.
-Comme pour toute librairie, inutile de réinventer la roue et de créer des doublons dans les packages, l'idée est vraiment d'ajouter des fonctionnalités à Atom afin d'enrichir notre expérience utilisateur d'Atom.
-s ALIVE!'; - message.classList.add('message'); - this.element.appendChild(message); - } - - // ... -} - -let myPackageView = new MyPackageView(state.myPackageViewState); -let modalPanel = atom.workspace.addModalPanel({ - item: myPackageView.getElement(), - visible: false; -}); - -modalPanel.show(); --
NotificationManager et Notification : Informez vos utilisateurs via des notifications
-Vous avez également la possibilité de rendre des notifications dans l'éditeur de plusieurs niveaux, avec les commandes suivantes :
-atom.notifications.addSuccess('My success notification'); -atom.notifications.addInfo('My info notification'); -atom.notifications.addWarning('My warning notification'); -atom.notifications.addError('My error notification'); -atom.notifications.addFatalError('My fatal error notification');-
-
GitRepository
-Celui-ci est très intéressant : vous pouvez en effet accéder à toutes les propriétés du repository Git actuellement utilisé par l'utilisateur.
-Vous pourrez alors obtenir (entre autres) la branche actuellement utilisée, obtenir l'URL du remote, voir si un fichier est nouveau ou modifié ou encore accéder au diff.
-let repository = atom.project.getRepositoryForDirectory('/path/to/project'); - -console.log(repository.getOriginURL()); // -> git@github.com:eko/atom-pull-request.git -console.log(repository.getShortHead()); // -> master -console.log(repository.isStatusNew('/path/to/file')); // -> true-
-
Encore bien d'autres choses à découvrir ...
-Je vous ai présenté les composants les plus courants mais je vous invite à visiter la documentation de l'API si vous souhaitez aller plus loin : https://atom.io/docs/api/latest/AtomEnvironment
--
Nous en arrivons au moment de tester notre package, et pour cela, Atom utilise Jasmine.
-Votre package vient déjà avec un fichier de test pré-défini :
-import MyPackageView from '../lib/my-package-view'; - -describe('MyPackageView', () => { - it('has one valid test', () => { - expect('life').toBe('easy'); - }); -});-
-
-
Les tests Jasmine doivent être structurés de la façon suivante :
-C'est maintenant à vous de jouer et de tester votre logique applicative.
--
Vous pouvez lancer les specs via le menu d'Atom : View -> Packages -> Run Package Specs .
--
Notre package est maintenant prêt à être publié !
- -Pour se faire, nous allons utiliser l'outil CLI installé avec Atom : apm .
-Après avoir pushé votre code sur un repository Github, rendez-vous dans le répertoire de votre package et jouez la commande suivante :
$ apm publish --tag v0.0.1 minor - -Preparing and tagging a new version ✓ -Pushing v0.0.1 tag ✓ -...-
-
La commande va s'occuper de créer et pusher le tag de la version spécifiée et référencer cette version sur le registry d'Atom.
-Félicitations, votre package est maintenant publié et visible à l'URL : https://atom.io/packages/<votre-package> !
--
Afin de vous assurer que votre package fonctionne toujours sur la version stable d'Atom mais également pour anticiper et tester également la version bêta, vous pouvez mettre en place Travis-CI sur le repository de votre code avec le fichier suivant :
-language: objective-c - -notifications: - email: - on_success: never - on_failure: change - -script: 'curl -s https://raw.githubusercontent.com/nikhilkalige/docblockr/develop/spec/atom-build-package.sh | sh' - -env: - global: - - APM_TEST_PACKAGES="" - - matrix: - - ATOM_CHANNEL=stable - - ATOM_CHANNEL=beta-
-
Je trouve personnellement qu'il s'agit d'une vraie révolution de pouvoir interagir à tel point avec l'éditeur de texte, l'outil utilisé la plupart du temps par les développeurs.
-L'API d'Atom est déjà très riche et est très simple à utiliser, c'est très certainement la raison pour laquelle la communauté offre déjà un bon nombre de packages.
-Comme pour toute librairie, inutile de réinventer la roue et de créer des doublons dans les packages, l'idée est vraiment d'ajouter des fonctionnalités à Atom afin d'enrichir notre expérience utilisateur d'Atom.
-{% endraw %} diff --git a/_drafts/2016-12-05-create-atom-package.html b/_drafts/2016-12-05-create-atom-package.html deleted file mode 100644 index 42959ae08..000000000 --- a/_drafts/2016-12-05-create-atom-package.html +++ /dev/null @@ -1,267 +0,0 @@ ---- -layout: post -title: Create your first Atom package -author: vcomposieux -date: '2016-12-05 17:34:21 +0100' -date_gmt: '2016-12-05 16:34:21 +0100' -categories: -- Non classé -tags: -- atom -- babel -- jasmine -- package ---- -{% raw %} -Atom is an open-source text editor (mostly used by developers) which is multi-platform and developed by GitHub company. It is based on Electron, the Github-developed framework, which allows developers to build native desktop applications for any operating systems by writing Javascript code.
-The main interesting feature of Atom is that it also has a great package management tool and packages are also written in Javascript so it's quite easy for anyone to create one. This article aims to talk about it.
-Finally, its community is also active as it already has a lot of available packages: 5 285 at this time.
-You can browse all packages by going to the following address: https://atom.io/packages
However, if you cannot find a package that fits your needs you can start creating your own and we will see how simple it is.
--
In order to create your own package, don't worry, you will not start from scratch. Indeed, we will use the Package Generator command which is brought to us by Atom core.
-To do that, you will just have to navigate into Packages -> Package Generator -> Generate Atom Package.
-[note]In order to generate your package, you can choose the language between Javascript and Coffeescript . This article will use Javascript.[/note]
-When the command is executed, Atom will open a new window into your package project, by default named my-package .
--
We will now see in details what's inside our package project directory:
-├── CHANGELOG.md -├── LICENSE.md -├── README.md -├── keymaps -│ └── my-package.json <- Key shortcuts registered by your package -├── lib -│ ├── my-package-view.js -│ └── my-package.js <- Entry point of your package -├── menus -│ └── my-package.json <- Menus declaration of your package into Atom application -├── package.json <- Description and library dependencies of your package -├── spec <- Tests directory (Jasmine) of your package -│ ├── my-package-spec.js -│ └── my-package-view-spec.js -└── styles <- Stylesheets used by your package -└── my-package.less-
The first element to add to your package is the package.json file which has to contain all information of your package such as its name, version, license type, keywords that will enable you to find your package into Atom registry and also your package dependancies.
-Please also note that there is a section called activationCommands which allows to define the executed command when your package is loaded.
-Next, we have the keymaps/my-package.json file which allows you to define shortcuts into your package very easily. Here is the default example:
-{ - "atom-workspace": { - "ctrl-alt-p": "my-package:toggle" - } -}-
Next, we will go into your package entry point. It is located into lib/my-package.js file.
-This file exports a default object which contains a subscriptions property and also activate() and deactivate() methods.
-During package activation (inside activate() method), we will register a CompositeDisposable type object inside our subscriptions property and that will allow us to add and maybe later remove some commands offered by our package:
-activate(state) { - this.subscriptions = new CompositeDisposable(); - this.subscriptions.add(atom.commands.add('atom-workspace', { - 'my-package:toggle': () => this.toggle() - })); -}-
Now that our command is registered, we can test it by simply typing the following words, into the Atom command palette: My Package: Toggle .
-This command will execute the code contained in the toggle() method of the class and will display a little modal at the top of the window.
You can add as many commands as you want and I really encourage you to decouple your code.
--
The Config component allows your package to have some settings.
-To add a new setting, you just have to define a config property into your package's class which is an object containing each settings definition, as follows:
-config: { - "gitlabUrl": { - "description": "If you rely on a private Gitlab server, please type your base URI here (default: https://gitlab.com).", - "type": "string", - "default": "https://gitlab.com" - } -}-
Atom settings allow multiple setting types (boolean , color , integer , string , ...) so it can fit your needs on setting values by your users.
-Once it is added, if you reload your package, you will see your package settings appearing into Atom settings:
- -In order to retrieve the value (or default value) defined by a user for a given setting in your code, you just have to use the following line:
-let gitlabUrl = atom.config.get('gitlabUrl'); -- -
So you are now ready to develop your package. We will have a quick overview of some interesting components that Atom brings to you and allows you to use in your package.
-TextEditor: Interact with the text editor
-With the TextEditor component, you will be able to insert some text into user's text editor, to save the current file, to go back and forth the history, to move the cursor into editor, to copy/paste into clipboard, to play with line indentation, to scroll, and to do so much more...
-Here are some examples to insert text in a specific position and to save the file automatically:
-editor.setCursorBufferPosition([row, column]); -editor.insertText('foo'); -editor.save();-
ViewRegistry & View: Create and display your own window
-These components allow you to create views (modals / windows) inside Atom and display them.
-You have an example of a modal View into the default package:
export default class MyPackageView { - constructor(serializedState) { - // Create root element - this.element = document.createElement('div'); - this.element.classList.add('my-package'); - - // Create message element - const message = document.createElement('div'); - message.textContent = 'The MyPackage package is Alive! It -NotificationManager & Notification: Alert your users with notifications
-Your package can also display a variety of notifications from "success" to "fatal error":
-atom.notifications.addSuccess('My success notification'); -atom.notifications.addInfo('My info notification'); -atom.notifications.addWarning('My warning notification'); -atom.notifications.addError('My error notification'); -atom.notifications.addFatalError('My fatal error notification');-GitRepository
-This one is also really interesting: indeed, you can access all the git properties of the current git repository that is used.
-This way, you will be able to access the current branch name, the repository remote URL and also see if a file is considered as a new or modified file. Let's see it in action:
-let repository = atom.project.getRepositoryForDirectory('/path/to/project'); - -console.log(repository.getOriginURL()); // -> git@github.com:eko/atom-pull-request.git -console.log(repository.getShortHead()); // -> master -console.log(repository.isStatusNew('/path/to/file')); // -> true-And more things to discover...
-We just made a review of the components that I played with but I invite you to read more on the following link if you want to go further: https://atom.io/docs/api/latest/AtomEnvironment
--
Test your package with specs
-Our package is now developed but we don't have to forget about the tests. To do that, Atom uses Jasmine.
-Your default package already has a prepared test file:
-import MyPackageView from '../lib/my-package-view'; - -describe('MyPackageView', () => { - it('has one valid test', () => { - expect('life').toBe('easy'); - }); -});-Jasmine specs tests are written in the following way:
-
This is now your turn to play with Jasmine and test your package logic.
-In order to run the specs tests, you just have to navigate into the following menu: View -> Packages -> Run Package Specs .
--
Our package is now ready to be deployed! Let's send it.
- -To do that, we will use the apm CLI tool which comes with Atom when installing it.
-After pushing your code into a Github repository, simply go into your package directory and type the following command:
$ apm publish --tag v0.0.1 minor - -Preparing and tagging a new version ✓ -Pushing v0.0.1 tag ✓ -...-
This command will be in charge of creating the new version tag into repository and publish this version into the Atom registry.
-Congratulations, your package is now published and available on the following URL: https://atom.io/packages/<your-package>!
--
The final step is to ensure that your package will continue to work in the future when you or your contributors will add new features but also when Atom releases a new beta version. To do that, you can use Travis-CI on your repository with the following configuration:
-language: objective-c - -notifications: - email: - on_success: never - on_failure: change - -script: 'curl -s https://raw.githubusercontent.com/nikhilkalige/docblockr/develop/spec/atom-build-package.sh | sh' - -env: - global: - - APM_TEST_PACKAGES="" - - matrix: - - ATOM_CHANNEL=stable - - ATOM_CHANNEL=beta- -
I personally think that this is a little revolution to allow developers to make their own editor and bring the features they want.
-Moreover, the Atom API is already very rich and very simple to use and this is certainly the main reason why the community offers a large number of packages.
-To conclude, as for all libraries, it is not useful to reinvent the wheel by creating already existing packages. The idea is to add features if they don't already exists, in order to enrich your user experience.
-s ALIVE!'; - message.classList.add('message'); - this.element.appendChild(message); - } - - // ... -} - -let myPackageView = new MyPackageView(state.myPackageViewState); -let modalPanel = atom.workspace.addModalPanel({ - item: myPackageView.getElement(), - visible: false; -}); - -modalPanel.show(); -NotificationManager & Notification: Alert your users with notifications
-Your package can also display a variety of notifications from "success" to "fatal error":
-atom.notifications.addSuccess('My success notification'); -atom.notifications.addInfo('My info notification'); -atom.notifications.addWarning('My warning notification'); -atom.notifications.addError('My error notification'); -atom.notifications.addFatalError('My fatal error notification');-
GitRepository
-This one is also really interesting: indeed, you can access all the git properties of the current git repository that is used.
-This way, you will be able to access the current branch name, the repository remote URL and also see if a file is considered as a new or modified file. Let's see it in action:
-let repository = atom.project.getRepositoryForDirectory('/path/to/project'); - -console.log(repository.getOriginURL()); // -> git@github.com:eko/atom-pull-request.git -console.log(repository.getShortHead()); // -> master -console.log(repository.isStatusNew('/path/to/file')); // -> true-
And more things to discover...
-We just made a review of the components that I played with but I invite you to read more on the following link if you want to go further: https://atom.io/docs/api/latest/AtomEnvironment
--
Our package is now developed but we don't have to forget about the tests. To do that, Atom uses Jasmine.
-Your default package already has a prepared test file:
-import MyPackageView from '../lib/my-package-view'; - -describe('MyPackageView', () => { - it('has one valid test', () => { - expect('life').toBe('easy'); - }); -});-
Jasmine specs tests are written in the following way:
-This is now your turn to play with Jasmine and test your package logic.
-In order to run the specs tests, you just have to navigate into the following menu: View -> Packages -> Run Package Specs .
--
Our package is now ready to be deployed! Let's send it.
- -To do that, we will use the apm CLI tool which comes with Atom when installing it.
-After pushing your code into a Github repository, simply go into your package directory and type the following command:
$ apm publish --tag v0.0.1 minor - -Preparing and tagging a new version ✓ -Pushing v0.0.1 tag ✓ -...-
This command will be in charge of creating the new version tag into repository and publish this version into the Atom registry.
-Congratulations, your package is now published and available on the following URL: https://atom.io/packages/<your-package>!
--
The final step is to ensure that your package will continue to work in the future when you or your contributors will add new features but also when Atom releases a new beta version. To do that, you can use Travis-CI on your repository with the following configuration:
-language: objective-c - -notifications: - email: - on_success: never - on_failure: change - -script: 'curl -s https://raw.githubusercontent.com/nikhilkalige/docblockr/develop/spec/atom-build-package.sh | sh' - -env: - global: - - APM_TEST_PACKAGES="" - - matrix: - - ATOM_CHANNEL=stable - - ATOM_CHANNEL=beta- -
I personally think that this is a little revolution to allow developers to make their own editor and bring the features they want.
-Moreover, the Atom API is already very rich and very simple to use and this is certainly the main reason why the community offers a large number of packages.
-To conclude, as for all libraries, it is not useful to reinvent the wheel by creating already existing packages. The idea is to add features if they don't already exists, in order to enrich your user experience.
-{% endraw %} diff --git a/_drafts/2017-01-17-redux-structurez-vos-applications-front.html b/_drafts/2017-01-17-redux-structurez-vos-applications-front.html deleted file mode 100644 index 5b90eeddb..000000000 --- a/_drafts/2017-01-17-redux-structurez-vos-applications-front.html +++ /dev/null @@ -1,212 +0,0 @@ ---- -layout: post -title: 'Redux : Structurez vos applications front' -author: vcomposieux -date: '2017-01-17 10:09:00 +0100' -date_gmt: '2017-01-17 09:09:00 +0100' -categories: -- Non classé -- Javascript -tags: -- Javascript -- react -- redux -- vuejs ---- -{% raw %} -L'écosystème Javascript est très riche, beaucoup de développeurs mais aussi de frameworks et d'outils sont disponibles.
-Lorsque vous souhaitez développer une application, quel que soit son framework de rendu, vous allez vite être amené à vouloir architecturer votre projet afin de différencier et d'organiser les données des vues. C'est particulièrement le cas lorsque vous utilisez des frameworks de rendu de composants comme React ou VueJS.
-Historiquement, le besoin s'est fait sentir sur React et Facebook a donc ouvert les sources de son outil Flux.
-Le principe est le suivant :
- -Votre application déclare, pour chaque composant, les actions qui lui sont liées. Ces actions permettent de définir l'état de votre composant, stocké dans un store , qui permet de maintenir votre vue à jour.
-L'inconvénient est que dans ce cas, vous avez un store par composant. Ce modèle fonctionne pour React mais vous pouvez vous sentir limité sur certaines applications.
-Dan Abramov a donc lancé, en juin 2015, Redux, qui permet principalement de simplifier la gestion du store car il y a en effet qu'un seul store pour toute votre application dans Redux.
-Tous vos composants peuvent donc accéder à vos données.
-Pour plus d'informations sur les différences Redux / Flux, je vous invite à lire cette réponse de Dan.
--
Nous allons voir dans cet article comment mettre en place et utiliser Redux sur vos projets.
-Notez dès maintenant que la librairie peut être utilisée avec plusieurs librairies de rendu comme React ou VueJS.
Pour installer Redux, il vous faudra installer le package npm (ou yarn) redux .
-Si vous utilisez Redux sur une application React, il vous faudra également le package react-redux ou encore vue-redux s'il s'agit d'un projet VueJS.
$ yarn add redux-
Rien de plus, vous êtes prêt à utiliser Redux.
--
Comme décrit précédemment, il vous faudra initialiser un store qui va permettre de stocker l'état de votre application.
-Pour instancier ce store, il vous faudra passer un ou plusieurs reducers . Les reducers contiennent les méthodes qui effectuent le changement d'état de votre application.
-Ces changements d'état sont effectués lorsqu'une action est déclenchée sur votre application.
Voilà, nous avons là les 3 composantes d'une application structurée par Redux : des actions, des reducers et un store.
-Nous allons prendre un cas pratique simple : un compteur que l'on peut incrémenter ou décrémenter d'une certaine valeur.
--
Voici l'arborescence que nous ciblons :
-src/ -├── actions -│ └── counter.js -├── constants -│ └── ActionTypes.js -├── reducers -│ ├── another.js -│ ├── counter.js -│ └── index.js -└── store - └── configureStore.js-
-
Écrivons donc un fichier d'actions qui permet de définir ces deux actions : incrémenter et décrémenter.
-Avant tout, nous allons également stocker ces noms d'actions dans des constantes, ce qui nous permettra d'être clair dans notre code car nous ferons toujours appel à ces constantes.
-Créez donc un fichier src/constants/ActionTypes.js avec le contenu :
-export const INCREMENT = 'INCREMENT'; -export const DECREMENT = 'DECREMENT';-
-
Nous allons maintenant écrire les définitions des actions. Créez maintenant le fichier src/actions/counter.js :
-import * as types from '../constants/ActionTypes'; - -export const increment = (value) => ({ type: types.INCREMENT, value }); -export const decrement = (value) => ({ type: types.DECREMENT, value });-
-
Vous venez de déclarer deux actions (increment et decrement ) qui prennent chacune un type (obligatoire) et une valeur à ajouter ou soustraire.
--
Il nous faut maintenant écrire les méthodes des reducers permettant de mettre à jour l'état de notre application.
-Ces reducers seront écrits dans le fichier src/reducers/counter.js :
-import { INCREMENT, DECREMENT } from '../constants/ActionTypes'; - -const initialState = { - current: 0, -}; - -export default function counter(state = initialState, action) { - switch (action.type) { - case INCREMENT: - return { - current: state.current += action.value, - }; - - case DECREMENT: - return { - current: state.current -= action.value, - }; - - default: - return state; - } -}-
-
-
Vous avez compris l'idée, nous avons nos actions dans un switch() { case ... } et mettons directement à jour les valeurs de notre store.
-Vous remarquerez que nous avons créés un état initial (initialState) afin d'initialiser les valeurs de notre application.
[note]Note : Il vous est possible de créer autant de reducers que nécessaire.[/note]
--
Si vous avez déclaré plusieurs reducers dans votre application, vous pouvez les combiner dans un fichier src/reducers/index.js comme suit :
--
import { combineReducers } from 'redux'; - -import counter from './counter'; -import another from './another'; - -const reducers = combineReducers({ - counter, - another, -}); - -export default reducers;-
-
Maintenant que nous avons nos actions et reducers, dernière étape indispensable : la création du store !
-Créez un fichier src/store/configureStore.js avec le contenu suivant :
-import { createStore } from 'redux'; -import reducers from '../reducers'; - -const configureStore = () => { - return createStore( - reducers, - ); -}; - -export default configureStore;-
-
Nous utilisons ici la fonction createStore() de l'API Redux permettant de créer notre store.
--
Afin d'aller un peu plus loin, notez que cette fonction peut prendre jusqu'à 3 arguments :
-Un middleware permet d'exécuter une callback à chaque fois que le dispatch() d'actions est exécuté.
--
Voici un exemple de middleware permettant de logger chaque action déclenchée :
-import { createStore, applyMiddleware } from 'redux' -import reducers from '../reducers'; - -function logger({ getState }) { - return (next) => (action) => { - console.log('will dispatch', action) - return next(action) - } -} - -const configureStore = () => { - return createStore( - reducers, - applyMiddleware(logger) - ); -}; - -export default configureStore;-
-
N'oubliez pas d'utiliser la fonction applyMiddleware() lorsque vous passez vos fonctions de middleware au store.
--
Le principe reste exactement le même lorsque Redux est utilisé avec React, cependant, la librairie react-redux va vous apporter des petites choses en plus.
-Vous allez en effet pouvoir lier l'état de votre application gérée par Redux ainsi que les actions que vous avez définies avec les props de vos composants React.
-Prenons un composant Counter reflétant l'architecture Redux mise en place dans notre cas d'exemple :
-import React, { PropTypes } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - -import * as CounterActions from '../actions/counter'; - -const Counter = ({ children, value, actions }) => ( --- - - - --); - -Counter.propTypes = { - children: PropTypes.object.isRequired, - value: PropTypes.number.isRequired, - actions: PropTypes.object.isRequired, -}; - -const mapStateToProps = state => ({ - value: state.counter.current, -}); - -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(CounterActions, dispatch), -}); - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(Counter);
-
De cette façon, nous récupérons donc les valeurs de nos props provenant de notre store mais également une propriété actions permettant d'appeler nos actions Redux.
-Les principaux éléments à noter ici sont :
-Ces deux fonctions sont ensuite appliquées à l'aide de la fonction connect() fournie par react-redux .
-[note]Note : Nous devons ici utiliser bindActionCreators() sur nos CounterActions car il s'agit d'un objet dont les valeurs sont des actions et cette fonction va permettre d'ajouter un appel à la fonction dispatch() de Redux afin que celles-ci soient correctement déclenchées.[/note]
--
Si nous mettons en parallèle les 1 303 720 téléchargements sur le mois précédent de la librairie Redux avec les 2 334 221 de téléchargements pour React, nous remarquons que Redux est aujourd'hui très utilisé et semble vraiment très apprécié par les développeurs car il s'agit d'une solution simple qui permet réellement de structurer une application front.
-Redux apporte, selon moi, une vraie solution permettant de structurer des applications au métier complexe aux communautés comme React, VueJS mais également aux autres.
-{% endraw %} diff --git a/_drafts/2017-01-20-redux-structure-frontend-applications.html b/_drafts/2017-01-20-redux-structure-frontend-applications.html deleted file mode 100644 index d543d9f93..000000000 --- a/_drafts/2017-01-20-redux-structure-frontend-applications.html +++ /dev/null @@ -1,196 +0,0 @@ ---- -layout: post -title: 'Redux: Structure your frontend applications' -author: vcomposieux -date: '2017-01-20 12:12:34 +0100' -date_gmt: '2017-01-20 11:12:34 +0100' -categories: -- Non classé -tags: -- Facebook -- Javascript -- react -- redux ---- -{% raw %} -Javascript ecosystem is really rich: full of developers but also full of frameworks and libraries.
-When you want to develop a frontend application, whatever its rendering framework, you will have to structure things into your project in order to organize the data management with views. This case occurs particularly when you use component rendering frameworks like React or VueJS.
-Historically, this has been needed by React so that's why Facebook has open sourced its tool named Flux.
-Here is the philosophy:
- -Your application declare actions for each components. These actions allow you to define the state of your data which is stored in a store . This stores continually maintains your view up-to-date.
-We have a drawback in this case because you have to define one store per component. This is working but on large applications you can feel limited with it.
-In June 2015, Dan Abramov has launched Redux which simplify store management because you only have one store for all your application.
-All of your application components can access to the whole state.
-For more information about Redux/Flux differences I encourage you to have a look at Dan's answer on this subject.
--
This article will deal about how to install and use Redux on your own projects.
-Please keep in mind that Redux can be used with multiple rendering frameworks like React or VueJS.
To install Redux, you will just need the redux npm (or yarn) package.
-If you use Redux into a React application, you will also need the react-redux package or even the vue-redux if you want to use it on a VueJS project.
$ yarn add redux-
Nothing more, you can now start to use Redux.
--
As previously described, you will have to instanciate a new store that will allow to store the state of all your application.
-In order to instanciate this store, you will have to give to it some reducers . Reducers contain methods that change the state of your application.
-These state changes occur when an action is dispatched by your application.
Here we are, we have the 3 things needed by a Redux application: actions, reducers and a store.
-We will use a simple practical case: a counter that we can increment or decrement with a given value.
-Here is our target arborescence:
-src/ -├── actions -│ └── counter.js -├── constants -│ └── ActionTypes.js -├── reducers -│ ├── another.js -│ ├── counter.js -│ └── index.js -└── store - └── configureStore.js-
Let's write an actions containing file that will implement our 2 actions: increment and decrement.
-Before all, we will store these actions names into constants in order to keep our code clear and comprehensible as we will always call these constants in all of our code.
-Start by creating a src/constants/ActionTypes.js file with the following content:
-export const INCREMENT = 'INCREMENT'; -export const DECREMENT = 'DECREMENT';-
Great, we will now write actions that correspond to these constants in a src/actions/counter.js file:
-import * as types from '../constants/ActionTypes'; - -export const increment = (value) => ({ type: types.INCREMENT, value }); -export const decrement = (value) => ({ type: types.DECREMENT, value });-
You have just created your 2 actions (increment and decrement) which each have a type property (required) and a value to add or remove to the current counter value.
-We will now write reducers functions that correspond to the actions we previously wrote in order to update the value in our application state.
-This will be written in the src/reducers/counter.js file:
-import { INCREMENT, DECREMENT } from '../constants/ActionTypes'; - -const initialState = { - current: 0, -}; - -export default function counter(state = initialState, action) { - switch (action.type) { - case INCREMENT: - return { - current: state.current += action.value, - }; - - case DECREMENT: - return { - current: state.current -= action.value, - }; - - default: - return state; - } -}-
You got the idea, we have our actions wrapped into a switch() { case ... } and directly return the store updated with new values.
-You can also observe that we have initialized an initial state (initialState) in order to prepare our application state with some default values.
[note]Note: You can write as many reducers as you need in your application so you can clearly split your code application.[/note]
-Only point if you declare multiple reducers into your application is that you will have to combine them here in a file named src/reducers/index.js as follows:
-import { combineReducers } from 'redux'; - -import counter from './counter'; -import another from './another'; - -const reducers = combineReducers({ - counter, - another, -}); - -export default reducers;-
You have your actions and your reducers so let's dive into the final step: store creation!
-Store will be created in a src/store/configureStore.js file with only these couple of lines:
-import { createStore } from 'redux'; -import reducers from '../reducers'; - -const configureStore = () => { - return createStore( - reducers, - ); -}; - -export default configureStore;-
You just have to call the Redux's createStore() API function in order to create your store.
-In order to go further, please note that this function can take a maximum of 3 arguments:
-A middleware is a callback that is executed each time Redux can the dispatch() function so each time an action is triggered.
-Here is a simple middleware that logs each dispatched actions:
-import { createStore, applyMiddleware } from 'redux' -import reducers from '../reducers'; - -function logger({ getState }) { - return (next) => (action) => { - console.log('will dispatch', action) - return next(action) - } -} - -const configureStore = () => { - return createStore( - reducers, - applyMiddleware(logger) - ); -}; - -export default configureStore;-
Do not forget to call the applyMiddleware() function when you pass your function to the store argument.
--
Principles are exactly the same when you want to use Redux on a React application. However, the react-redux library brings some cool additional features to fit with React.
-Indeed, thanks to this library, you will be able to map your React components props with the Redux state and actions.
-Let's take a concrete case: a Counter component which could be a component for our previous use case:
-import React, { PropTypes } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - -import * as CounterActions from '../actions/counter'; - -const Counter = ({ children, value, actions }) => ( --- - - - --); - -Counter.propTypes = { - children: PropTypes.object.isRequired, - value: PropTypes.number.isRequired, - actions: PropTypes.object.isRequired, -}; - -const mapStateToProps = state => ({ - value: state.counter.current, -}); - -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(CounterActions, dispatch), -}); - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(Counter);
This way, we are able to retrieve our props values which came from the Redux store but also an actions property that will allow us to dispatch Redux events when we will call it.
-Main things to note here are:
-These two functions are applied thanks to the connect() function brought by the react-redux library.
-[note]Note: We have to use the bindActionCreators() function over our CounterActions because this is an object that contains actions functions so this function will allows React to call the dispatch() Redux function when React will call the functions in order to have them correctly triggered.[/note]
--
If we put in parallel the download numbers of Redux (1 303 720 download over the previous month) with the 2 334 221 downloads of React, we can conclude that Redux is today very used and seems very much appreciated by developers because it's a simple solution that can greatly help you to structure your application.
-Redux brings, in my opinion, a real solution to structure complex (or large) business applications and bring that to the React and VueJS (and others) communities.
-{% endraw %} diff --git a/_drafts/2017-02-22-consul-service-discovery-failure-detection-2.html b/_drafts/2017-02-22-consul-service-discovery-failure-detection-2.html deleted file mode 100644 index f781e473b..000000000 --- a/_drafts/2017-02-22-consul-service-discovery-failure-detection-2.html +++ /dev/null @@ -1,206 +0,0 @@ ---- -layout: post -title: 'Consul : Service Discovery et Failure Detection' -author: vcomposieux -date: '2017-02-22 10:49:25 +0100' -date_gmt: '2017-02-22 09:49:25 +0100' -categories: -- Dev Ops -- Devops -tags: -- service -- consul -- discovery -- failure -- detection -- health -- check ---- -{% raw %} -Consul est un outil développé en Go par la société HashiCorp et a vu le jour en 2013.
-Consul a plusieurs composants mais son objectif principal est de regrouper la connaissance des services d'une architecture (service discovery) et permet de s'assurer que les services contactés sont toujours disponibles en s'assurant que la santé de ces services est toujours bonne (via du health check).
-
Concrètement, Consul va nous apporter un serveur DNS permettant de mettre à jour les adresses IP disponibles pour un service, en fonction de ceux qui sont en bonne santé. Ceci permet également de faire du load balancing bien que nous verrons qu'il ne permette pas pour le moment de préférer un service à un autre.
-Il offre également d'autres services tel que du stockage clé/valeur, nous l'utiliserons dans cet article afin que Docker Swarm y stocke ses valeurs.
--
Afin de clarifier la suite de cet article, voici les ports utilisés par Consul :
--
La suite de cet article va se concentrer sur la partie service discovery et failure detection. Nous allons pour cela mettre en place un cluster Docker Swarm possédant l'architecture suivante :
- -Nous aurons donc 3 machines Docker :
--
Nous mettrons également sur nos deux nodes (cluster Docker Swarm) un container Docker pour Registrator, permettant de faciliter l'enregistrement de nos services Docker sur Consul.
-Pour plus d'informations concernant Registrator, vous pouvez vous rendre sur : https://gliderlabs.com/registrator/
-Commençons à installer notre architecture !
--
-
Nous allons commencer par créer la première machine : notre Consul.
--
Pour cela, tapez :
-$ docker-machine create -d virtualbox consul-
-
Une fois la machine prête, préparez votre environnement pour utiliser cette machine et lancez un container Consul :
-$ eval $(docker-machine env consul) -$ docker run -d \ - -p 8301:8301 \ - -p 8302:8302 \ - -p 8400:8400 \ - -p 8500:8500 \ - -p 53:8600/udp \ - consul-
-
Nous avons maintenant notre Consul prêt à recevoir nos services et nos prochaines machines membres de notre cluster Docker Swarm.
-Vous pouvez d'ailleurs ouvrir l'interface web mise à disposition en obtenant l'ip de la machine Consul :
-$ docker-machine ip consul -<ip-obtenue>-
-
Puis ouvrez dans votre navigateur l'URL : http://<ip-obtenue>:8500.
--
Nous allons maintenant créer la machine correspondant au premier node de notre cluster Docker Swarm qui se verra également obtenir le rôle de master de notre cluster Swarm (il en faut bien un).
-$ docker-machine create -d virtualbox \ - --swarm \ - --swarm-master \ - --swarm-discovery="consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-advertise=eth1:2376" swarm-node-01-
-
Comme vous le voyez, nous précisons l'option --swarm-discovery avec l'IP de notre machine Consul et le port 8500 correspondant à l'API de Consul. Ainsi, Docker Swarm pourra utiliser l'API pour enregistrer les machines du cluster.
--
Nous allons maintenant configurer notre environnement pour utiliser cette machine et y installer dessus un container Registrator permettant d'auto-enregistrer les nouveaux services sur Consul.
--
Pour ce faire, tapez :
-$ eval $(docker-machine env swarm-node-01)-
puis :
-$ docker run -d \ - --volume=/var/run/docker.sock:/tmp/docker.sock \ - gliderlabs/registrator \ - -ip $(docker-machine ip swarm-node-01) \ - consul://$(docker-machine ip consul):8500-
-
Vous remarquez que nous partageons le socket Docker sur la machine. Cette solution peut être controversée mais dans le cas de cet article, passons là-dessus. Pour une architecture stable, nous préférerons enregistrer nous-même les services via l'API de Consul.
-L'option -ip permet de préciser à Registrator l'IP sur laquelle nous voulons accéder aux services, à savoir l'IP de la machine et non pas l'IP interne du container Docker.
Nous sommes prêts à démarrer notre service HTTP. Celui-ci est une simple image Docker "ekofr/http-ip" qui lance une application HTTP écrite en Go et qui affiche "hello, <ip>" avec l'adresse IP du container courant.
--
Pour le besoin de cet article, nous allons également créer un réseau différent entre les deux machines afin d'identifier des adresses IP différentes pour les deux services.
--
Créons donc un nouveau réseau pour notre node 01 :
-$ docker network create \ - --subnet=172.18.0.0/16 network-node-01-
puis utilisez ce réseau sur le container du service HTTP :
-$ docker run -d \ - --net network-node-01 \ - -p 80:8080 \ - ekofr/http-ip-
-
Avant d'exécuter les mêmes étapes pour créer notre node 02, assurons-nous d'avoir un service fonctionnel :
-$ curl http://localhost:80 -hello from 172.18.0.X-
-
Nous allons donc (presque) répéter les étapes du node 01 en changeant quelques valeurs seulement. Créez la machine :
-$ docker-machine create -d virtualbox \ - --swarm \ - --swarm-discovery="consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-advertise=eth1:2376" swarm-node-02-
-
Préparez votre environnement pour utiliser cette machine node 02 et installez-y Registrator :
-$ eval $(docker-machine env swarm-node-02)-
$ docker run -d \ - --volume=/var/run/docker.sock:/tmp/docker.sock \ - gliderlabs/registrator \ - -ip $(docker-machine ip swarm-node-02) \ - consul://$(docker-machine ip consul):8500-
-
Puis créez un nouveau réseau et lancez le service HTTP avec ce réseau :
-$ docker network create \ - --subnet=172.19.0.0/16 network-node-02-
$ docker run -d \ - --net network-node-02 \ - -p 80:8080 \ - ekofr/http-ip-
Nous voilà prêt à découvrir ce que nous apporte Consul.
--
Vous pouvez en effet maintenant résoudre votre service http-ip.service.consul en utilisant le serveur DNS apporté par Consul, vous devriez voir vos deux services enregistrés :
-$ dig @$(docker-machine ip consul) http-ip.service.consul - -;; QUESTION SECTION: -;http-ip.service.consul. IN A - -;; ANSWER SECTION: -http-ip.service.consul. 0 IN A 192.168.99.100 -http-ip.service.consul. 0 IN A 192.168.99.102-
-
Autrement dit, un load balancing sera fait sur un de ces deux services lorsque vous chercherez à joindre http://http-ip.service.consul .
-Oui, mais qu'en est-il du côté de la répartition de cette charge ? Pouvons-nous définir une priorité et/ou poids ?
-Malheureusement, la réponse est non, pas pour le moment. Une issue a cependant été ouverte sur Github pour demander le support de celui-ci : https://github.com/hashicorp/consul/issues/1088.
-
En effet, si nous regardons de plus près l'enregistrement DNS de type SRV , voici ce que nous obtenons :
-$ dig @$(docker-machine ip consul) http-ip.service.consul SRV - -;; ANSWER SECTION: -http-ip.service.consul. 0 IN SRV 1 1 80 c0a86366.addr.dc1.consul. -http-ip.service.consul. 0 IN SRV 1 1 80 c0a86364.addr.dc1.consul.-
-
Comme vous pouvez le voir, la priorité et le poids sont tous les deux définis à 1, le load balancing sera donc équilibré entre tous les services.
-Si vous ajoutez l'IP de la machine Consul en tant que serveur DNS sur votre système d'exploitation, vous pourrez donc appeler votre service en HTTP et vous rendre compte plus facilement du load balancing :
-$ curl http://http-ip.service.consul -hello from 172.18.0.2 - -$ curl http://http-ip.service.consul -hello from 172.19.0.2-
-
Nous avons ici une IP correspondant à chaque service HTTP que nous avons enregistré.
--
Nous allons maintenant ajouter un Health Check à notre service afin de s'assurer que celui-ci peut être utilisé.
-Nous allons donc commencer par retourner sur notre node 01 et supprimer le container ekofr/http-ip afin de le recréer avec un Health Check :
-$ eval $(docker-machine env swarm-node-01)-
$ docker kill \ -$(docker ps -q --filter='ancestor=ekofr/http-ip')-
-
Registrator nous offre des variables d'environnement afin d'ajouter des Health Check de nos containers à Consul, vous pouvez consulter la liste de toutes les variables disponibles ici : http://gliderlabs.com/registrator/latest/user/backends/#consul.
--
L'idée est pour nous de vérifier que le port 80 répond correctement, nous allons donc ajouter un script exécutant simplement une requête curl. Pour ce faire :
-$ docker run -d \ - --net network-node-01 -p 80:8080 \ - -e SERVICE_CHECK_SCRIPT="curl -s -f http://$(docker-machine ip swarm-node-01)" \ - -e SERVICE_CHECK_INTERVAL=5s \ - -e SERVICE_CHECK_TIMEOUT=1s \ - ekofr/http-ip-
-
Vous pouvez faire de même sur le node 02 (en faisant attention à bien modifier les node-01 en node-02 ) et vous devriez maintenant pouvoir visualiser ces checks sur l'interface Consul :
- --
De la même façon, vous pouvez également utiliser l'API de Consul afin de vérifier la santé de vos services :
-$ curl http://$(docker-machine ip consul):8500/v1/health/checks/http-ip .. -[ - { - "Status": "passing", - "Output": "hello from 172.18.0.2", - "ServiceName": "http-ip", - }, - ... -]-
-
Vous pouvez maintenant mettre en place Consul sur vos architectures afin de vous assurer que les services contactés sont bien disponibles mais surtout pouvoir identifier les éventuels problèmes qui peuvent survenir sur vos services.
-Il est donc important d'ajouter un maximum de checks sur les éléments pouvant rendre vos services indisponibles (vérifier que celui-ci peut bien être contacté, vérifier l'espace disque disponible sur la machine, etc ...).
-Consul est un outil qui s'intègre parfaitement dans vos architectures, grâce à son utilisation très simple et son API complète.
-{% endraw %} diff --git a/_drafts/2017-02-22-consul-service-discovery-failure-detection.html b/_drafts/2017-02-22-consul-service-discovery-failure-detection.html deleted file mode 100644 index d08b4674e..000000000 --- a/_drafts/2017-02-22-consul-service-discovery-failure-detection.html +++ /dev/null @@ -1,193 +0,0 @@ ---- -layout: post -title: 'Consul: Service Discovery and Failure Detection' -author: vcomposieux -date: '2017-02-22 10:50:16 +0100' -date_gmt: '2017-02-22 09:50:16 +0100' -categories: -- Non classé -tags: [] ---- -{% raw %} -Consul is a product developed in Go language by the HashiCorp company and was born in 2013.
-Consul has multiple components but its main goal is to manage the services knowledge in an architecture (which is service discovery) and also allows to ensure that all contacted services are always available by adding health checks on them.
Basically, Consul will bring us a DNS server that will resolve IP addresses of a host's services, depending on which one will be healthy.
--
This method also allows to do load balancing even if we will see in this blog post that it's actually not possible to distribute priorities between services.
--
Another Consul service we'll use in this article is key/value storage because we'll create a Docker Swarm cluster and will use Consul as the discovery/storage for Docker Swarm.
-In order to clarify the rest of the article, here are the ports used by Consul:
--
Next, we'll focus on service discovery and failure detection. To do that, we'll create a Docker Swarm cluster with the following architecture:
- -As you can see, we'll have 3 Docker machines:
--
We'll also add on our two nodes a Docker container with Registrator image that will facilitate the discovery of Docker containers into Consul.
-For more information about Registrator, you can visit: https://gliderlabs.com/registrator/.
-Let's start to install our architecture!
--
We'll start by creating our first machine: Consul
-To do that, just type:
-$ docker-machine create -d virtualbox consul-
-
Once the machine is fully ready, prepare your environment to use this Docker machine and launch a Consul container:
-$ eval $(docker-machine env consul) -$ docker run -d \ - -p 8301:8301 \ - -p 8302:8302 \ - -p 8400:8400 \ - -p 8500:8500 \ - -p 53:8600/udp \ - consul-
-
Well, your Consul is ready to receive your services and also our Docker Swarm nodes!
-By the way, you can open the web interface by obtaining the Consul machine IP address:
-$ docker-machine ip consul -<obtained-ip>-
-
Then, open in your browser the following URL: http://<obtained-ip>:8500 .
--
Now, it's time to create the machine that corresponds to our first Docker Swarm cluster node and that will also receive the master role for our cluster (we need one...):
-$ docker-machine create -d virtualbox \ - --swarm \ - --swarm-master \ - --swarm-discovery="consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-advertise=eth1:2376" swarm-node-01-
-
As you can see, we've added the --swarm-discovery option with our Consul machine IP address and port 8500 that corresponds to the Consul API. This way, Docker Swarm will use the Consul API to store machine information with the rest of the cluster.
-We'll now configure our environment to use this machine and install a Registrator container on top of it in order to auto-register our new services (Docker containers).
-To do that, type the following:
-$ eval $(docker-machine env swarm-node-01)-
-
Then:
-$ docker run -d \ - --volume=/var/run/docker.sock:/tmp/docker.sock \ - gliderlabs/registrator \ - -ip $(docker-machine ip swarm-node-01) \ - consul://$(docker-machine ip consul):8500-
-
Here, you can notice that we share the host Docker socket in the container. This solution could be a controversial solution but in our example case, forgive me about that ;)
-If you want to register services to Consul I recommend to register them using the Consul API in order to keep control on what's added in your Consul.
-The -ip option allows to precise to Registrator that we want to register our services with the given IP address: so here the Docker machine IP address and not the Docker container internal IP address.
-We are now ready to start our HTTP service. This one is located under the "ekofr/http-ip" Docker image which is a simple Go HTTP application that renders "hello, <ip>" with the IP address of the current container.
-In order to fit this article needs, we will also create a different network for the two Docker machines in order to clearly identify IP addresses for our two services.
-Let's create a new network concerning our first node:
-$ docker network create \ - --subnet=172.18.0.0/16 network-node-01-
-
then you can use the newly created network to be used with your HTTP service container:
-$ docker run -d \ - --net network-node-01 \ - -p 80:8080 \ - ekofr/http-ip-
-
Before executing the same steps for our second node, we will ensure that our HTTP service works:
-$ curl http://localhost:80 -hello from 172.18.0.X-
-
We'll now repeat most steps we've ran for our first node but we'll change some values. First, create the Docker machine:
-$ docker-machine create -d virtualbox \ - --swarm \ - --swarm-discovery="consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \ - --engine-opt="cluster-advertise=eth1:2376" swarm-node-02-
-
Prepare your environment to use this new node 02 machine and install Registrator container on it:
-$ eval $(docker-machine env swarm-node-02)-
$ docker run -d \ - --volume=/var/run/docker.sock:/tmp/docker.sock \ - gliderlabs/registrator \ - -ip $(docker-machine ip swarm-node-02) \ - consul://$(docker-machine ip consul):8500-
-
Now, create the new network and launch your HTTP service:
-$ docker network create \ - --subnet=172.19.0.0/16 network-node-02-
$ docker run -d \ - --net network-node-02 \ - -p 80:8080 \ - ekofr/http-ip-
We are all set! We can now discover what Consul brings to us.
--
Indeed, you can now resolve your service hostname `http-ip.service.consul` by using the DNS server brought by Consul and you should see your two services appearing as a DNS record:
-$ dig @$(docker-machine ip consul) http-ip.service.consul - -;; QUESTION SECTION: -;http-ip.service.consul. IN A - -;; ANSWER SECTION: -http-ip.service.consul. 0 IN A 192.168.99.100 -http-ip.service.consul. 0 IN A 192.168.99.102-
-
In other words, a kind of load balancing will be done on one of these services when you'll try to join them http://http-ip.service.consul .
-Ok, but what about the load balancing repartition? Are we able to define a priority and/or weight for each services?
-Sadly, the answer is no, actually. An issue is currently opened about that on Github in order to bring this support. You can find it here: https://github.com/hashicorp/consul/issues/1088.
-
Indeed, if we look in details about SRV DNS record type, here is what we get:
-$ dig @$(docker-machine ip consul) http-ip.service.consul SRV - -;; ANSWER SECTION: -http-ip.service.consul. 0 IN SRV 1 1 80 c0a86366.addr.dc1.consul. -http-ip.service.consul. 0 IN SRV 1 1 80 c0a86364.addr.dc1.consul.-
-
As you can see here, priority and weight are both defined to value 1 so the load balancing will be equal between all services.
-If you add the Consul Docker machine IP address as a DNS server on your operating system, you'll be able to perform HTTP calls and see more easily what's happening on load balancing:
-$ curl http://http-ip.service.consul -hello from 172.18.0.2 - -$ curl http://http-ip.service.consul -hello from 172.19.0.2-
Here, we have an IP address that corresponds to each HTTP service that we have registered so we can clearly see that we are load balanced between our two containers.
--
We'll now add a Health Check to our service in order to ensure that it can be use safely by our users.
-In this case we'll start to return on our node 01 and suppress the container named ekofr/http-ip in order to recreate it with a Health Check:
-$ eval $(docker-machine env swarm-node-01)-
$ docker kill \ - $(docker ps -q --filter='ancestor=ekofr/http-ip')-
Registrator brings us some environment variables in order to add some Health Check for our containers into Consul and you can see the full list here: http://gliderlabs.com/registrator/latest/user/backends/#consul.
--
Idea here is to verify that port 80 is opened and application answers correctly so we'll add a script that simply executes a curl command:
-$ docker run -d \ - --net network-node-01 -p 80:8080 \ - -e SERVICE_CHECK_SCRIPT="curl -s -f http://$(docker-machine ip swarm-node-01)" \ - -e SERVICE_CHECK_INTERVAL=5s \ - -e SERVICE_CHECK_TIMEOUT=1s \ - ekofr/http-ip-
-
You can do the same thing on your node 02 (by paying attention to modify the node-01 values to node-02 ) and you should now visualize these checks on the Consul web UI:
- -You can also use the Consul API in order to verify the good health of your services:
-$ curl http://$(docker-machine ip consul):8500/v1/health/checks/http-ip .. -[ - { - "Status": "passing", - "Output": "hello from 172.18.0.2", - "ServiceName": "http-ip", - }, - ... -]-
-
You are now able to install Consul on your projects architectures in order to ensure that services you contact are available and also be able to identify eventual issues that can occur on your services.
-It's important to add a maximum of checks on elements that can make your services become unavailable (ensure that this one can be contacted and answer, ensure that remaining available disk space is sufficient, etc...).
-Consul is a tool that integrates well in your architectures by its simplicity of use and its powerful API.
-{% endraw %} diff --git a/_posts/2016-07-19-behat-structure-functional-tests.md b/_posts/2016-07-19-behat-structure-functional-tests.md new file mode 100644 index 000000000..f71f5ced5 --- /dev/null +++ b/_posts/2016-07-19-behat-structure-functional-tests.md @@ -0,0 +1,356 @@ +--- +layout: post +title: 'Behat: structure your functional tests' +permalink: /en/behat-structure-functional-tests/ +author: vcomposieux +date: '2016-07-19 14:15:31 +0200' +date_gmt: '2016-07-19 12:15:31 +0200' +categories: + - Symfony + - Php +tags: + - symfony + - php + - behat +--- +In order to ensure that your application is running well, it's important to write functional tests. + +Behat is the most used tool with Symfony to handle your functional tests and that's great because it's really a complete suite. + +You should nevertheless know how to use it wisely in order to cover useful and complete test cases and that's the goal of this blog post. + +# Introduction + +## Functional testing: what's that? +When we are talking about functional testing we often mean that we want to automatize human-testing scenarios over the application. + +However, it is important to write the following test types to cover the functional scope: +* `Interface tests`: Goal here is to realize interface verifications to ensure that our web application features can be used over a browser, +* `Integration tests`: Goal of these tests is to ensure that sour code (already unit-tested) which makes the application running is acting as it should when all components are linked together. + +Idea is to develop and run both integration tests and interface tests with Behat. +Before we can go, please note that we will use a `Selenium` server which will receive orders by `Mink` (a Behat extension) and will pilot our browser (Chrome in our configuration). + +To be clear on the architecture we will use, here is a scheme that will resume the role of all elements: + + +## Behat set up +First step is to install Behat and its extensions as dependencies in our `composer.json` file: + +```json +"require-dev": { + "behat/behat": "~3.1", + "behat/symfony2-extension": "~2.1", + "behat/mink": "~1.7", + "behat/mink-extension": "~2.2", + "behat/mink-selenium2-driver": "~1.3", + "emuse/behat-html-formatter": "dev-master" +} +``` + +In order to make your future contexts autoloaded, you also have to add this little `PSR-4` section: + +```json +"autoload-dev": { + "psr-4": { + "Acme\Tests\Behat\Context\": "features/context/" + } +} +``` + +Now, let's create our **behat.yml** file in our project root directory in order to define our tests execution. + +Here is the configuration file we will start with: + +```yaml +default: + suites: ~ + extensions: + Behat\Symfony2Extension: ~ + Behat\MinkExtension: + base_url: "http://acme.tld/" + selenium2: + browser: chrome + wd_host: 'http://selenium-host:4444/wd/hub' + default_session: selenium2 + emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension: + name: html + renderer: Twig,Behat2 + file_name: index + print_args: true + print_outp: true + loop_break: true + formatters: + pretty: ~ + html: + output_path: %paths.base%/web/reports/behat +``` + +We will talk of all of these sections in their defined order so let's start with the **suites** section which is empty at this time but we will implement it later when we will have some contexts to add into it. + +Then, we load some Behat extensions: + +* `Behat\Symfony2Extension` will allow us to inject Symfony services into our contexts (useful for integrations tests mostly), +* `Behat\MinkExtension` will allow us to pilot Selenium (drive itself the Chrome browser) so we fill in all the necessary information like the hostname, the Selenium server port number and the base URL we will use for testing, +* `emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension` will generate a HTML report during tests execution (which is great to show to our customer for instance). + +Finally, in the `formatters` section we keep the `pretty` formatter in order to keep an output in our terminal and the HTML reports will be generated at the same time in the `web/reports/behat` directory in order to make them available over HTTP (it should not be a problem as you should not execute functional tests in production, be careful to restrict access in this case). +Now that Behat is ready and configured we will prepare our functional tests that we will split into two distinct Behat suites: `integration` and `interface`. + +# Writing functional tests (features) +In our example, we will write tests in order to ensure that a new user can register over a registration page. +We will have to start by writing our tests scenarios (in a `.feature` file) that we will put into a `features/` directory located at the project root directory. + +So for instance, we will have the following scenario: + +File: `features/registration/register.feature`: + +{% raw %} +``` +Feature: Register + In order to create an account + As a user + I want to be able to register on the application + +Scenario: I register when I fill my username and password only + Given I am on the registration page + And I register with username "johndoe" and password "azerty123" + When I submit the form + Then I should see the registration confirmation +``` +{% endraw %} + +# Integration tests + +As said previously, these tests are here to ensure all code written for the registration page can be executed and linked without any errors. + +To do so, we will create a new integration context that concerns the registration part under directory `features/context/registration`: + +File: `features/context/registration/IntegrationRegisterContext`: + +{% raw %} +```php +registerer = $registerer; + } + + /** + * @Given I am on the registration page + */ + public function iAmOnTheRegistrationPage() + { + $this->user = new User(); + } + + /** + * @Given /I register with username "(?Pn{% raw %}ndefault: + suites: ~ + extensions: + Behat\Symfony2Extension: ~ + Behat\MinkExtension: + base_url: "http://acme.tld/" + selenium2: + browser: chrome + wd_host: 'http://selenium-host:4444/wd/hub' + default_session: selenium2 + emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension: + name: html + renderer: Twig,Behat2 + file_name: index + print_args: true + print_outp: true + loop_break: true + formatters: + pretty: ~ + html: + output_path: %paths.base%/web/reports/behat +``` +{% endraw %} + +Si nous prenons les sections dans leur ordre, nous avons avant tout une section `suites` pour le moment vide mais que nous allons alimenter par la suite de cet article. + +Ensuite, nous chargeons ici plusieurs extensions de Behat : + +* L'extension `Behat\Symfony2Extension` permettant notamment d'injecter des services Symfony dans nos classes contextes de test, +* L'extension `Behat\MinkExtension` qui va nous permettre de piloter notre Selenium (qui pilotera lui-même notre navigateur Chrome), nous lui fournissons donc les informations nécessaires tels que le host et port du serveur Selenium ainsi que la base de l'URL à contacter, +* L'extension `emuse\BehatHTMLFormatter\BehatHTMLFormatterExtension` qui nous permettra de générer un rapport HTML lors du lancement des tests (toujours sympa à présenter au client). + +Notons enfin que dans la section **formatters**, nous conservons le formatter **pretty** afin d'avoir une sortie sympa sur notre terminal et que les rapports HTML seront quant à eux générés dans le répertoire `web/reports/behat` afin qu'ils soient accessibles en HTTP (à priori pas de soucis car vous ne devriez pas jouer ces tests en production, attention à la restriction d'accès si c'est le cas). + +Maintenant que Behat est prêt et configuré, nous allons préparer nos tests fonctionnels que nous allons découper en deux "suites" Behat distinctes : `integration` et `interface`. + +# Ecriture des tests fonctionnels (features) + +Nous allons partir sur des tests permettant de s'assurer du bon fonctionnement d'une page d'inscription. + +Nous devons avant tout écrire nos scénarios de tests fonctionnels (fichier **.feature**) que nous allons placer dans un répertoire **features/** à la racine du projet. + +Nous allons donc avoir, par exemple, le scénario suivant : + +Fichier : `features/registration/register.feature` : +{% raw %} +``` +Feature: Register + In order to create an account + As a user + I want to be able to register on the application + +Scenario: I register when I fill my username and password only + Given I am on the registration page + And I register with username "johndoe" and password "azerty123" + When I submit the form + Then I should see the registration confirmation +``` +{% endraw %} + +## Tests d'intégration + +Il va maintenant convenir d'implémenter le code qui va nous permettre de tester que le code écrit pour l'inscription d'un utilisateur peut être exécuté et enchaîné sans erreur. + +Nous allons donc créer un contexte d'intégration propre à l'inscription sous le répertoire `features/context/registration` : + +Fichier : `features/context/registration/IntegrationRegisterContext` : + +{% raw %} +```php +registerer = $registerer; + } + + /** + * @Given I am on the registration page + */ + public function iAmOnTheRegistrationPage() + { + $this->user = new User(); + } + + /** + * @Given /I register with username "(?P+``` + +puis : + +```bash +$ docker run -d \ + --volume=/var/run/docker.sock:/tmp/docker.sock \ + gliderlabs/registrator \ + -ip $(docker-machine ip swarm-node-01) \ + consul://$(docker-machine ip consul):8500 +``` + +Vous remarquez que nous partageons le socket Docker sur la machine. Cette solution peut être [controversée](https://www.lvh.io/posts/dont-expose-the-docker-socket-not-even-to-a-container.html mais dans le cas de cet article, passons là-dessus. Pour une architecture stable, nous préférerons enregistrer nous-même les services via l'API de Consul. +L'option `-ip` permet de préciser à Registrator l'IP sur laquelle nous voulons accéder aux services, à savoir l'IP de la machine et non pas l'IP interne du container Docker. + +Nous sommes prêts à démarrer notre service HTTP. Celui-ci est une simple image Docker "ekofr/http-ip" qui lance une application HTTP écrite en Go et qui affiche "hello,[^"]*)" and password "(?P [^"]*)"/ + */ + public function iRegisterWithUsernameAndPassword($username, $password) + { + $this->user->setUsername($username); + $this->user->setPassword($password); + } + + /** + * @When I submit the form + */ + public function iSubmitTheForm() + { + $this->response = $this->registerer->register($this->user); + } + + /** + * @Then I should see the registration confirmation message + */ + public function iShouldSeeTheRegistrationConfirmation() + { + if (!$this->response) { + throw new \RuntimeException('User is not registered.'); + } + } +} +``` +{% endraw %} + +L'implémentation du test d'intégration est terminé pour cette feature ! +Passons maintenant au test d'interface ! + +## Tests d'interface + +Ce test va se baser sur la même feature et nous n'avons absolument rien modifié dans le test précédemment écrit. C'est pourquoi il est important de bien rédiger ses tests fonctionnels afin qu'ils restent assez génériques pour être implémentés à la fois en test d'intégration et en test d'interface. + +Créons donc le contexte qui sera utilisé pour le test d'interface (préfixé par Mink dans notre cas, mais vous pouvez préfixer par ce que vous voulez) sous le même répertoire `features/context/registration` : + +Fichier : `features/context/registration/MinkRegisterContext` : + +{% raw %} +```php +visit('/register'); + } + + /** + * @Given /I register with username "(?P [^"]*)" and password "(?P [^"]*)"/ + */ + public function iRegisterWithUsernameAndPassword($username, $password) + { + $this->fillField('registration[username]', $username); + $this->fillField('registration[password]', $password); + } + + /** + * @When I submit the form + */ + public function iSubmitTheForm() + { + $this->pressButton('Register'); + } + + /** + * @Then I should see the registration confirmation message + */ + public function iShouldSeeTheRegistrationConfirmation() + { + $this->assertPageContainsText('Congratulations, you are now registered!'); + } +} +``` +{% endraw %} + +Nous venons d'implémenter un test d'interface basé sur le même scénario que celui que nous avons utilisé pour notre test d'intégration, reprenant exactement les quatre méthodes implémentées précédemment avec les mêmes annotations Behat. + +La seule différence est que dans ce contexte, Mink va demander à Selenium d'effectuer les actions au niveau de l'interface de notre application en pilotant un navigateur au lieu de tester le code lui-même. + +# Définitions des contextes + +Il ne nous reste plus qu'à ajouter les contextes créés précédemment sous notre section **suites** dans le fichier de configuration `behat.yml` : + +{% raw %} +```yaml + suites: + integration: + paths: + - %paths.base%/features/registration + contexts: + - Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext: + - "@acme.registration.registerer" + interface: + paths: + - %paths.base%/features/registration + contexts: + - Behat\MinkExtension\Context\MinkContext: [] + - Acme\Tests\Behat\Context\Registration\MinkRegisterContext: [] +``` +{% endraw %} + +Il est important de voir ici que nous découpons clairement les tests en deux suites distinctes : `integration` et `interface` : chacune d'entre elles sera exécutée avec les contextes qui lui sont propres. + +Etant donné que nous avons chargés l'extension Symfony2 lors de la mise en place de Behat, nous avons la possibilité d'injecter des services Symfony dans nos contextes, c'est le cas ici avec le service `acme.registration.registerer`. + +# Exécution des tests + +Pour lancer tous les tests, exécutez simplement, à la racine du projet : `bin/behat -c behat.yml`. +Pour lancer uniquement la suite d'integration, par exemple : `bin/behat -c behat.yml --suite=integration`. + +Le rapport HTML est quand à lui généré dans `web/reports/behat/`, comme spécifié dans notre configuration, ce qui vous permettra d'avoir un aperçu rapide des tests qui échouent, plutôt pratique lorsque vous avez de nombreux tests. + +# Lier plusieurs contextes entre eux** + +Pour terminer, vous pourrez parfois avoir besoin de lier les contextes entre eux. Par exemple, imaginons que vous ayez une deuxième page sur votre formulaire d'inscription pour renseigner les informations personnelles, vous allez alors créer deux nouveaux contextes **IntegrationProfileContext** et `MinkProfileContext`. + +Partons sur le contexte d'intégration pour simplifier l'explication, l'idée est de ne pas dupliquer le code précédemment créé et permettant de tester la première étape `IntegrationRegisterContext` et de réutiliser ces informations dans le nouveau contexte `IntegrationProfileContext`. + +Ceci est possible grâce à l'annotation `@BeforeScenario` de Behat. + +Fichier : `features/context/registration/IntegrationProfileContext` : + +{% raw %} +```php +getEnvironment(); + + $this->registerContext = $environment->getContext( + 'Acme\Tests\Behat\Context\Registration\IntegrationRegisterContext' + ); + } +} +``` +{% endraw %} + +Vous avez maintenant à disposition une propriété `$registerContext` et pouvez accéder à des informations qui proviennent du contexte précédent. + +# Conclusion + +Tout part de l'écriture des tests fonctionnels qui doivent être bien réfléchis pour ensuite permettre une implémentation technique à la fois en test d'intégration mais aussi en test d'interface. + +La structure choisie pour classer ses tests fonctionnels est aussi importante pour pouvoir s'y retrouver rapidement dans les différents scénarios de test lorsque l'application prend de l'ampleur. diff --git a/_posts/2016-09-27-utiliser-le-composant-workflow-de-symfony.md b/_posts/2016-09-27-utiliser-le-composant-workflow-de-symfony.md new file mode 100644 index 000000000..c5f946288 --- /dev/null +++ b/_posts/2016-09-27-utiliser-le-composant-workflow-de-symfony.md @@ -0,0 +1,193 @@ +--- +layout: post +title: Utiliser le composant Workflow de Symfony +permalink: /fr/utiliser-le-composant-workflow-de-symfony/ +author: vcomposieux +date: '2016-09-27 11:05:28 +0200' +date_gmt: '2016-09-27 09:05:28 +0200' +categories: + - Symfony + - Php +tags: + - symfony + - php + - workflow +--- +Depuis Symfony 3.2, un nouveau composant très utile a vu le jour : [le composant Workflow](http://symfony.com/blog/new-in-symfony-3-2-workflow-component). +Celui-ci est en effet très pratique et peut très largement simplifier vos développements lorsque vous avez, par exemple, à gérer des workflows de statut dans votre application. + +# Installation + +Dans tous les cas, vous devez installer la dépendance suivante : + +```json +"symfony/workflow": "~3.2@dev" +``` + +Si vous utilisez une version antérieure de Symfony mais >=2.3, c'est aussi possible mais il vous faudra également installer ce bundle non-officiel qui embarque le composant et ajoute la configuration nécessaire sous le namespace du bundle : + +```json +"fduch/workflow-bundle": "~0.2@dev" +``` + +Pensez bien à activer le bundle dans votre kernel. + +# Configuration + +Il va maintenant nous falloir définir la configuration de notre workflow et ainsi définir les statuts (appelés places) et transitions possibles. +Pour cet article, nous sommes partis sur un exemple basé sur les statuts d'une pull request. Celle-ci peut avoir les états suivants : `opened` , `closed` , `needs_review` , `reviewed` et enfin `merged`. + +Cependant, elle ne pourra, par exemple, pas être passée en `merged` sans être passée par le statut `reviewed` . C'est ici que le composant Workflow prend tout son sens. + +Voici ce que donne notre configuration complète : +```yaml +workflow: + workflows: + pull_request: + marking_store: + type: multiple_state + arguments: + - state + supports: + - AppBundle\Entity\PullRequest + places: + - opened + - closed + - needs_review + - reviewed + - merged + transitions: + feedback: + from: opened + to: needs_review + review: + from: [opened, needs_review] + to: reviewed + merge: + from: reviewed + to: merged + close: + from: [opened, needs_review, reviewed] + to: closed +``` + +Nous spécifions ici que nous souhaitons utiliser un workflow de type `multiple_state` . Notez que si vous souhaitez utiliser une transition simple d'un statut vers un autre, vous pouvez utiliser ici `single_state`. +Nous disposons donc également d'une classe `AppBundle\Entity\PullRequest` qui dispose d'une propriété `state` ainsi que son setter et getter associé (le composant va utiliser les méthodes getter et setter pour changer l'état et/ou obtenir l'état courant) : + +```php +state = $state; + } + + public function getState() + { + return $this->state; + } +} +``` + +Nous avons terminé, nous pouvons maintenant commencer à utiliser le composant Workflow ! + +# Utilisation + +La première chose utile à effectuer après avoir écrit votre workflow est de générer une représentation graphique de celui-ci (sous un format [Graphviz](http://www.graphviz.org)). + +Pour ce faire, nous utilisons la commande Symfony : + +```bash +$ bin/console workflow:dump pull_request +``` + +Celle-ci va vous générer un code Graphviz qui donne le schéma suivant : + + + +Celui-ci permet vraiment de donner une vision claire de son workflow, à tous les niveaux (développeurs, product owners, clients, ...). +Le composant Workflow implémente des méthodes permettant d'effectuer une transition, vérifier si une transition peut être effectuée avec l'état actuel et lister les transitions possibles avec l'état actuel. + +Pour vérifier si vous pouvez effectuer une transition et l'appliquer, rien de plus simple : + +```php +getPullRequestManager()->find($identifier); + + // Nous obtenons le service "workflow. " + $workflow = $this->get('workflow.pull_request'); + + if ($workflow->can($pullRequest, 'merge')) { + $workflow->apply($pullRequest, 'merge'); + } + + ... + } +} +``` + +Si vous ne passez pas par la méthode `can()` , la méthode `apply()` renverra une exception si la transition ne peut pas être effectuée. Vous pouvez donc également catcher cette exception de type `Symfony\Component\Workflow\Exception\LogicException`. + +Pour lister les transitions disponibles : + +```php +$workflow->getEnabledTransitions($pullRequest); +``` + +Globalement, l'utilisation du composant se limite à ces 3 méthodes. Comme vous le remarquez, il devient très simple d'utiliser un workflow, même complexe ! + +# Branchez-vous sur les événements ! + +Le composant utilise également plusieurs événements, à savoir, dans l'ordre chronologique : + +* `workflow.leave` : lorsque notre pull request va se voir dépourvue de son dernier statut, +* `workflow.transition` : lorsque la transition vers le nouvel état est lancée, +* `workflow.enter` : lorsque le nouvel état est défini sur notre pull request, +* `workflow.guard` : pour vous éviter de rendre la transition possible, vous pouvez utiliser cet événement pour définir votre événement bloqué : `$event->setBlocked(true);` + +Enfin, sachez que ces événements existent aussi en version unique pour chaque workflow afin de vous permettre de vous brancher dessus uniquement sur certains workflows. Il vous faut alors utiliser le nom `workflow.pull_request.enter`. + +Faisons encore mieux, vous pouvez même vous brancher sur une transition particulière : + +* `workflow.pull_request.enter.needs_review` : permet de se brancher uniquement lorsque nous définissons un nouvel état `needs_review` à notre pull request, nous pourrons alors envoyer un email à l'auteur pour qu'il corrige certaines choses, +* `workflow.pull_request.transition.merge` : interviendra lorsque la transition de merge prendra effet sur notre pull request. + +# Conclusion + +Le composant Workflow est vraiment très utile dans la gestion d'états ou de statuts sur la plupart des projets. + +N'hésitez pas à l'utiliser, sa facilité de configuration et d'utilisation vous aidera grandement sur vos projets. +Aussi, il m'a permis de donner un graphique clair sur un workflow complexe à toutes les personnes avec qui je travaillais. diff --git a/_posts/2016-09-29-symfony-workflow-component.md b/_posts/2016-09-29-symfony-workflow-component.md new file mode 100644 index 000000000..f73cf23ae --- /dev/null +++ b/_posts/2016-09-29-symfony-workflow-component.md @@ -0,0 +1,194 @@ +--- +layout: post +title: Use the Symfony Workflow component +permalink: /en/symfony-workflow-component/ +author: vcomposieux +date: '2016-09-29 10:04:20 +0200' +date_gmt: '2016-09-29 08:04:20 +0200' +categories: + - Symfony + - Php +tags: + - symfony + - php + - workflow +--- +Since Symfony 3.2, a new useful component was born: the [Workflow component](http://symfony.com/blog/new-in-symfony-3-2-workflow-component). +It is indeed really convenient and can simplify greatly your developments when you have to manage status workflows in your application, that occurs a lot. + +# Installation + +In all cases, you have to install the following dependency: + +```json +"symfony/workflow": "~3.2@dev" +``` + +If you use an earlier version of Symfony but >=2.3 you are also able to use this component, but you have to install the following non-official bundle, which loads the component itself and add the required configuration under the bundle's namespace: + +```json +"fduch/workflow-bundle": "~0.2@dev" +``` + +Do not forget to enable the bundle in your kernel class. + +# Configuration + +Time has come to write our workflow configuration. We will have to define all our places (statuses / states) and available transitions. +In this blog post, we will take a pull request status example. A pull request can have one of the following status: `opened` , `closed` , `needs_review` , `reviewed` or `merged`. +However, it cannot be, for instance, moved from the `merged` status without having the `reviewed` status before. The workflow component makes sense here. + +Here is our full workflow configuration: +```yaml +workflow: + workflows: + pull_request: + marking_store: + type: multiple_state + arguments: + - state + supports: + - AppBundle\Entity\PullRequest + places: + - opened + - closed + - needs_review + - reviewed + - merged + transitions: + feedback: + from: opened + to: needs_review + review: + from: [opened, needs_review] + to: reviewed + merge: + from: reviewed + to: merged + close: + from: [opened, needs_review, reviewed] + to: closed +``` + +Here, we specify we want to use a `multiple_state` workflow. Please not that if you want to use a simple transition from one state to another, you can use a `single_state`. +For this example, we also have defined a `AppBundle\Entity\PullRequest` class which has a `state` property and associated setter and getter methods (component will use these methods to manage transitions): + +```php +state = $state; + } + + public function getState() + { + return $this->state; + } +} +``` + + +Everything is now ready, we can start to use the Workflow component! + +# Usage + +First useful thing to do after you have written your workflow configuration is to generate a graph using the Symfony command. The command will generate one graph using the [Graphviz](http://www.graphviz.org) format. + +Here is the Symfony command you have to run: + +```bash +$ bin/console workflow:dump pull_request +``` + +The generated Graphviz will give you the following diagram: + + + +This one gives you a really clear vision of your workflow and allows everyone at every level (developers, product owners, customers, ...) to understand the business logic. +The Workflow component implements methods that allow you to verify if a transition is applicable and to later apply it depending on the current status and to also list all enabled transitions. + +In order to check if you can apply a specific transition and apply it, simply use the following code: + +```php +getPullRequestManager()->find($identifier); + + // Nous obtenons le service "workflow. " + $workflow = $this->get('workflow.pull_request'); + + if ($workflow->can($pullRequest, 'merge')) { + $workflow->apply($pullRequest, 'merge'); + } + + ... + } +} +``` + +In the case you do not want to use the `can()` method, the `apply()` ``` one``` will throw an exception if the transition cannot be effectively done, so you will be able to catch exceptions on the `Symfony\Component\Workflow\Exception\LogicException` type. + + +To list all enabled transitions: + +```php +$workflow->getEnabledTransitions($pullRequest); +``` + +Overall, the component usage is just as simple as these 3 methods. As you can see, complex workflows are now easier to manage! + +# Tune in for events! + +The component also dispatches multiple events, chronologically sorted as: + +* `workflow.leave`: when our pull request will leave its current state, +* `workflow.transition`: when the transition to the new state is launched, +* `workflow.enter`: when the new state is just defined on our pull request, +* `workflow.guard`: this one allows you to prevent the asked transition from occurring, you can do that by calling the following method: `$event->setBlocked(true);` + +Finally, you have to know that these events also exist in a unique way for each workflow in order to allow you to tune in your workflow events only. +If you want to do that, you have to listen to the following name: `workflow.pull_request.enter`. + +Let's do better than that: you are also able to listen to a specific transition or a state for a specific workflow: + +* `workflow.pull_request.enter.needs_review`: is only dispatched when a `needs_review` state is defined on our pull request. We could for instance send an email to the author with the changes the reviewer has suggested, +* `workflow.pull_request.transition.merge`: will occur when the merge transition will be dispatched on our pull request. + +# Conclusion + +The Workflow component is a really useful component to manage state or status on most of web applications. +Do not hesitate to use it because its simplicity of configuration and use will probably help you a lot on your projects. +Also, this component helps me a lot to give people I was working with a clear vision on a complex workflow we have to manage. The graph generation allows to clarify all of that for everyone! diff --git a/_posts/2016-12-01-creer-votre-premier-package-pour-atom.md b/_posts/2016-12-01-creer-votre-premier-package-pour-atom.md new file mode 100644 index 000000000..e80e69b88 --- /dev/null +++ b/_posts/2016-12-01-creer-votre-premier-package-pour-atom.md @@ -0,0 +1,269 @@ +--- +layout: post +title: Créer votre premier package pour Atom +permalink: /fr/creer-votre-premier-package-pour-atom/ +author: vcomposieux +date: '2016-12-01 12:14:17 +0100' +date_gmt: '2016-12-01 11:14:17 +0100' +categories: + - Javascript +tags: + - atom + - package + - babel + - jasmine +--- + +# Introduction à Atom + +[Atom](https://atom.io) est un éditeur de texte (principalement utilisé pour du code) multi-plateforme développé par la société GitHub et qui s'appuie sur un autre framework développé par GitHub : Electron, qui permet de développer des applications natives pour chaque système d'exploitation à partir de code Javascript. + +Le grand intérêt d'Atom est qu'il peut être étendu très facilement avec un peu de code Javascript et c'est ce que nous allons voir dans cet article. Ainsi, tout le monde peut écrire son "package" pour Atom. + +Aussi, sa communauté très active compte déjà un bon nombre de packages : `5 285` au moment où j'écris cet article. +Vous pouvez les retrouver à l'URL suivante : [https://atom.io/packages](https://atom.io/packages) + +Si toutefois vous ne trouvez pas votre bonheur dans les packages déjà proposés, vous pouvez alors écrire le votre et nous allons voir qu'il n'y a rien de compliqué. + +# Générer son premier package + +Pour créer votre premier package, rassurez-vous, vous n'allez pas partir de rien. En effet, nous allons utiliser la commande fournie par le package `Package Generator` natif à Atom. +Pour se faire, il vous suffira de naviguer dans : `Packages` -> `Package Generator` -> `Generate Atom Package`. + +Lors de la génération, vous pouvez choisir le langage que vous souhaitez utiliser pour développer votre package, entre `Javascript` et `Coffeescript`. Cet article est rédigé en Javascript. + +Atom vous ouvrira alors une nouvelle fenêtre à l'intérieur de votre nouveau package, nommé `my-package`. + +# Structure d'un package + +Nous allons maintenant détailler la structure par défaut du projet : + +``` +├── CHANGELOG.md +├── LICENSE.md +├── README.md +├── keymaps +│ └── my-package.json <- Raccourcis clavier enregistrés par votre package +├── lib +│ ├── my-package-view.js +│ └── my-package.js <- Point d'entrée de votre package +├── menus +│ └── my-package.json <- Déclaration des menus que votre package ajoute dans Atom +├── package.json <- Fichier descriptif et de dépendances de votre package +├── spec <- Répertoire de tests (Jasmine) de votre package +│ ├── my-package-spec.js +│ └── my-package-view-spec.js +└── styles <- Feuilles de styles utilisées par votre package +└── my-package.less +``` + +Le premier élément à renseigner est le fichier `package.json` qui doit contenir les informations relatives à votre package tel que son nom, sa version, license, mots clés pour trouver votre package et également ses librairies de dépendances. +Notez également la présence dans ce fichier d'une section `activationCommands` qui vous permet de définir la commande exécutée lors de l'activation de votre package. + +Nous avons ensuite le fichier `keymaps/my-package.json` qui vous permet d'enregistrer des raccourcis clavier dans votre application, de façon très simple : + +```json +{ + "atom-workspace": { + "ctrl-alt-p": "my-package:toggle" + } +} +``` + +Passons maintenant au point d'entrée de votre package. Il s'agit de ce qui se trouve dans `lib/my-package.js`. +Dans ce fichier est exporté un objet par défaut qui contient une propriété `subscriptions` et des méthodes `activate()` et `deactivate()` notamment. + +Lors de l'activation de notre package (dans la méthode `activate()` ), nous allons enregistrer dans notre propriété `subscriptions` un objet de type [CompositeDisposable](https://atom.io/docs/api/latest/CompositeDisposable) qui nous permettra d'ajouter et d'éventuellement plus tard, supprimer des commandes disponibles dans notre package : + +```js +activate(state) { + this.subscriptions = new CompositeDisposable(); + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'my-package:toggle': () => this.toggle() + })); +} +``` + +Notre commande étant enregistrée, nous pouvons dès maintenant l'exécuter en ouvrant la palette de commande : `My Package: Toggle`. + +Celle-ci va exécuter le code contenu dans la méthode `toggle()` de votre classe, soit dans le package par défaut, afficher une petite fenêtre en haut de l'écran. +Vous pouvez ajouter autant de commandes que vous le souhaitez et surtout, découper votre code comme vous le sentez. + +## Ajouter des paramètres dans mon package + +Vous avez la possibilité d'ajouter des paramètres à votre package et ceci est rendu possible grâce au composant [Config](https://atom.io/docs/api/latest/Config). + +Il vous suffira d'ajouter une propriété `config` à votre classe en définissant un objet avec la définition de chaque élément que vous souhaitez voir apparaître dans vos paramètres : + +```json +config: { + "gitlabUrl": { + "description": "If you rely on a private Gitlab server, please type your base URI here (default: https://gitlab.com).", + "type": "string", + "default": "https://gitlab.com" + } +} +``` + +La configuration offre un grand nombre de valeurs disponibles (`boolean` , `color` , `integer` , `string` , ...) ce qui permet de laisser un grand nombre de choix à vos utilisateurs. +Les paramètres de votre package apparaîtront alors pour votre package, sous Atom : + + + +Vous pourrez alors, à tout moment dans votre code, obtenir dans votre package la valeur définie par l'utilisateur (ou la valeur par défaut fournie si aucune valeur n'a été renseignée) via : + +```js +let gitlabUrl = atom.config.get('gitlabUrl'); +``` + +# Tour d'horizon des composants + +Vous pouvez maintenant commencer à développer votre package, nous allons donc parcourir les différents composants qui sont à votre disposition et que vous pourrez utiliser dans votre package ! + +## TextEditor : Agissez sur l'éditeur de texte + +Avec le composant `TextEditor` , vous allez pouvoir insérer du texte dans votre éditeur, enregistrer le fichier, jouer sur l'historique des actions (aller en avant ou arrière), déplacer le curseur dans l'éditeur, copier/coller dans le presse-papier, jouer sur l'indentation, scroller, etc ... +Quelques commandes en exemple, ici pour insérer du texte à une coordonnée donnée et enregistrer le fichier : + +```js +editor.setCursorBufferPosition([row, column]); +editor.insertText('foo'); +editor.save(); +``` + +## ViewRegistry et View : Créez et affichez votre propre fenêtre + +Ces composants vont vous permettre de créer votre fenêtre à l'intérieur d'Atom et de l'afficher. +Vous disposez d'un exemple d'utilisation du composant `View` dans le package généré par défaut : + +```js +export default class MyPackageView { + constructor(serializedState) { + // Create root element + this.element = document.createElement('div'); + this.element.classList.add('my-package'); + + // Create message element + const message = document.createElement('div'); + message.textContent = 'The MyPackage package is Alive! It\'s ALIVE!'; + message.classList.add('message'); + this.element.appendChild(message); + } + + // ... +} + +let myPackageView = new MyPackageView(state.myPackageViewState); +let modalPanel = atom.workspace.addModalPanel({ +item: myPackageView.getElement(), +visible: false; +}); + +modalPanel.show(); +``` + +## NotificationManager et Notification : Informez vos utilisateurs via des notifications + +Vous avez également la possibilité de rendre des notifications dans l'éditeur de plusieurs niveaux, avec les commandes suivantes : + +```js +atom.notifications.addSuccess('My success notification'); +atom.notifications.addInfo('My info notification'); +atom.notifications.addWarning('My warning notification'); +atom.notifications.addError('My error notification'); +atom.notifications.addFatalError('My fatal error notification'); +``` + +## GitRepository + +Celui-ci est très intéressant : vous pouvez en effet accéder à toutes les propriétés du repository Git actuellement utilisé par l'utilisateur. +Vous pourrez alors obtenir (entre autres) la branche actuellement utilisée, obtenir l'URL du remote, voir si un fichier est nouveau ou modifié ou encore accéder au diff. + +```js +let repository = atom.project.getRepositoryForDirectory('/path/to/project'); + +console.log(repository.getOriginURL()); // -> git@github.com:eko/atom-pull-request.git +console.log(repository.getShortHead()); // -> master +console.log(repository.isStatusNew('/path/to/file')); // -> true +``` + +## Encore bien d'autres choses à découvrir ... +Je vous ai présenté les composants les plus courants mais je vous invite à visiter la documentation de l'API si vous souhaitez aller plus loin : [https://atom.io/docs/api/latest/AtomEnvironment](https://atom.io/docs/api/latest/AtomEnvironment) + +# Tester votre package + +Nous en arrivons au moment de tester notre package, et pour cela, Atom utilise [Jasmine](https://jasmine.github.io) + +Votre package vient déjà avec un fichier de test pré-défini : + +```js +import MyPackageView from '../lib/my-package-view'; + +describe('MyPackageView', () => { + it('has one valid test', () => { + expect('life').toBe('easy'); + }); +}); +``` + + +Les tests Jasmine doivent être structurés de la façon suivante : + +* `describe()` : Une suite de test commence par une fonction describe qui prend un nom en premier argument et une fonction en deuxième argument, +* `it()` : Une spécification est ajoutée par ce mot clé, il doit être contenu à l'intérieur d'une suite de test, +* `expect()` : Il s'agit d'une assertion, lorsqu'on s'attend à avoir un résultat donné. + +C'est maintenant à vous de jouer et de tester votre logique applicative. + +Vous pouvez lancer les specs via le menu d'Atom : `View` -> `Packages` -> `Run Package Specs`. + +# Publier votre package + +Notre package est maintenant prêt à être publié ! + + + +Pour se faire, nous allons utiliser l'outil CLI installé avec Atom : `apm`. + +Après avoir pushé votre code sur un repository Github, rendez-vous dans le répertoire de votre package et jouez la commande suivante : + +```bash +$ apm publish --tag v0.0.1 minor + +Preparing and tagging a new version ✓ +Pushing v0.0.1 tag ✓ +... +``` + +La commande va s'occuper de créer et pusher le tag de la version spécifiée et référencer cette version sur le registry d'Atom. +Félicitations, votre package est maintenant publié et visible à l'URL : `https://atom.io/packages/ ` ! + +# Intégration continue + +Afin de vous assurer que votre package fonctionne toujours sur la version stable d'Atom mais également pour anticiper et tester également la version bêta, vous pouvez mettre en place [Travis-CI](https://travis-ci.org) sur le repository de votre code avec le fichier suivant : + +```yaml +language: objective-c + +notifications: + email: + on_success: never + on_failure: change + +script: 'curl -s https://raw.githubusercontent.com/nikhilkalige/docblockr/develop/spec/atom-build-package.sh | sh' + +env: + global: + - APM_TEST_PACKAGES="" + + matrix: + - ATOM_CHANNEL=stable + - ATOM_CHANNEL=beta< +``` + +# Conclusion + +Je trouve personnellement qu'il s'agit d'une vraie révolution de pouvoir interagir à tel point avec l'éditeur de texte, l'outil utilisé la plupart du temps par les développeurs. +L'API d'Atom est déjà très riche et est très simple à utiliser, c'est très certainement la raison pour laquelle la communauté offre déjà un bon nombre de packages. + +Comme pour toute librairie, inutile de réinventer la roue et de créer des doublons dans les packages, l'idée est vraiment d'ajouter des fonctionnalités à Atom afin d'enrichir notre expérience utilisateur d'Atom. diff --git a/_posts/2016-12-05-create-atom-package.md b/_posts/2016-12-05-create-atom-package.md new file mode 100644 index 000000000..def16518f --- /dev/null +++ b/_posts/2016-12-05-create-atom-package.md @@ -0,0 +1,268 @@ +--- +layout: post +title: Create your first Atom package +permalink: /en/create-atom-package/ +author: vcomposieux +date: '2016-12-05 17:34:21 +0100' +date_gmt: '2016-12-05 16:34:21 +0100' +categories: + - Javascript +tags: + - atom + - babel + - jasmine + - package +--- +# Introduction to Atom +[Atom](https://atom.io) is an open-source text editor (mostly used by developers) which is multi-platform and developed by GitHub company. It is based on Electron, the Github-developed framework, which allows developers to build native desktop applications for any operating systems by writing Javascript code. + +The main interesting feature of Atom is that it also has a great package management tool and packages are also written in Javascript so it's quite easy for anyone to create one. This article aims to talk about it. +Finally, its community is also active as it already has a lot of available packages: `5 285` at this time. +You can browse all packages by going to the following address: [https://atom.io/packages](https://atom.io/packages). + +However, if you cannot find a package that fits your needs you can start creating your own and we will see how simple it is. + +# Generate your first package + +In order to create your own package, don't worry, you will not start from scratch. Indeed, we will use the `Package Generator` command which is brought to us by Atom core. +To do that, you will just have to navigate into `Packages` -> `Package Generator` -> `Generate Atom Package`. + +In order to generate your package, you can choose the language between `Javascript` and `Coffeescript` . This article will use Javascript. + +When the command is executed, Atom will open a new window into your package project, by default named `my-package`. + +# Package structure + +We will now see in details what's inside our package project directory: + +``` +├── CHANGELOG.md +├── LICENSE.md +├── README.md +├── keymaps +│ └── my-package.json <- Key shortcuts registered by your package +├── lib +│ ├── my-package-view.js +│ └── my-package.js <- Entry point of your package +├── menus +│ └── my-package.json <- Menus declaration of your package into Atom application +├── package.json <- Description and library dependencies of your package +├── spec <- Tests directory (Jasmine) of your package +│ ├── my-package-spec.js +│ └── my-package-view-spec.js +└── styles <- Stylesheets used by your package +└── my-package.less +``` + +The first element to add to your package is the `package.json` file which has to contain all information of your package such as its name, version, license type, keywords that will enable you to find your package into Atom registry and also your package dependancies. + +Please also note that there is a section called `activationCommands` which allows to define the executed command when your package is loaded. + +Next, we have the `keymaps/my-package.json` file which allows you to define shortcuts into your package very easily. Here is the default example: + +```json +{ + "atom-workspace": { + "ctrl-alt-p": "my-package:toggle" + } +} +``` + +Next, we will go into your package entry point. It is located into `lib/my-package.js` file. +This file exports a default object which contains a `subscriptions` property and also `activate()` and `deactivate()` methods. + +During package activation (inside `activate()` method), we will register a CompositeDisposable type object inside our `subscriptions` property and that will allow us to add and maybe later remove some commands offered by our package: + +```js +activate(state) { + this.subscriptions = new CompositeDisposable(); + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'my-package:toggle': () => this.toggle() + })); +} +``` + +Now that our command is registered, we can test it by simply typing the following words, into the Atom command palette: `My Package: Toggle`. +This command will execute the code contained in the `toggle()` method of the class and will display a little modal at the top of the window. +You can add as many commands as you want and I really encourage you to decouple your code. + +# Add settings for your package + +The [Config](https://atom.io/docs/api/latest/Config) component allows your package to have some settings. + +To add a new setting, you just have to define a `config` property into your package's class which is an object containing each settings definition, as follows: + +```json +config: { + "gitlabUrl": { + "description": "If you rely on a private Gitlab server, please type your base URI here (default: https://gitlab.com).", + "type": "string", + "default": "https://gitlab.com" + } +} +``` + +Atom settings allow multiple setting types (`boolean` , `color` , `integer` , `string` , ...) so it can fit your needs on setting values by your users. + +Once it is added, if you reload your package, you will see your package settings appearing into Atom settings: + + + +In order to retrieve the value (or default value) defined by a user for a given setting in your code, you just have to use the following line: + +```js +let gitlabUrl = atom.config.get('gitlabUrl'); +``` + +# Components overview + +So you are now ready to develop your package. We will have a quick overview of some interesting components that Atom brings to you and allows you to use in your package. + +## TextEditor: Interact with the text editor + +With the `TextEditor` component, you will be able to insert some text into user's text editor, to save the current file, to go back and forth the history, to move the cursor into editor, to copy/paste into clipboard, to play with line indentation, to scroll, and to do so much more... + +Here are some examples to insert text in a specific position and to save the file automatically: +```js +editor.setCursorBufferPosition([row, column]); + +editor.insertText('foo'); +editor.save(); +``` + +## ViewRegistry & View: Create and display your own window + +These components allow you to create views (modals / windows) inside Atom and display them. + +You have an example of a modal `View` into the default package: + +```js +export default class MyPackageView { + constructor(serializedState) { + // Create root element + this.element = document.createElement('div'); + this.element.classList.add('my-package'); + + // Create message element + const message = document.createElement('div'); + message.textContent = 'The MyPackage package is Alive! It\'s ALIVE!'; + message.classList.add('message'); + this.element.appendChild(message); + } + + // ... +} + +let myPackageView = new MyPackageView(state.myPackageViewState); +let modalPanel = atom.workspace.addModalPanel({ +item: myPackageView.getElement(), +visible: false; +}); + +modalPanel.show(); +``` + +## NotificationManager & Notification: Alert your users with notifications + +Your package can also display a variety of notifications from "success" to "fatal error": + +```js +atom.notifications.addSuccess('My success notification'); +atom.notifications.addInfo('My info notification'); +atom.notifications.addWarning('My warning notification'); +atom.notifications.addError('My error notification'); +atom.notifications.addFatalError('My fatal error notification'); +``` + +## GitRepository + +This one is also really interesting: indeed, you can access all the git properties of the current git repository that is used. +This way, you will be able to access the current branch name, the repository remote URL and also see if a file is considered as a new or modified file. Let's see it in action: + +```js +let repository = atom.project.getRepositoryForDirectory('/path/to/project'); + +console.log(repository.getOriginURL()); // -> git@github.com:eko/atom-pull-request.git +console.log(repository.getShortHead()); // -> master +console.log(repository.isStatusNew('/path/to/file')); // -> true +``` + +## And more things to discover... + +We just made a review of the components that I played with but I invite you to read more on the following link if you want to go further: [https://atom.io/docs/api/latest/AtomEnvironment](https://atom.io/docs/api/latest/AtomEnvironment). + +## Test your package with specs + +Our package is now developed but we don't have to forget about the tests. To do that, Atom uses [Jasmine](https://jasmine.github.io). + +Your default package already has a prepared test file: + +```js +import MyPackageView from '../lib/my-package-view'; + +describe('MyPackageView', () => { + it('has one valid test', () => { + expect('life').toBe('easy'); + }); +}); +``` + +Jasmine specs tests are written in the following way: + +* `describe()` : A Jasmine test suite starts with a "describe" function which takes a name as the first argument and a function as the second, +* `it()` : A specification is added by using this function, "it" has to be contained into a specification, +* `expect()` : This one is an assertion, when we expect something to happen. + +This is now your turn to play with Jasmine and test your package logic. +In order to run the specs tests, you just have to navigate into the following menu: `View` -> `Packages` -> `Run Package Specs`. + +# Publish your package + +Our package is now ready to be deployed! Let's send it. + + + +To do that, we will use the `apm` CLI tool which comes with Atom when installing it. + +After pushing your code into a Github repository, simply go into your package directory and type the following command: + +```bash +$ apm publish --tag v0.0.1 minor + +Preparing and tagging a new version ✓ +Pushing v0.0.1 tag ✓ +... +``` + +This command will be in charge of creating the new version tag into repository and publish this version into the Atom registry. +Congratulations, your package is now published and available on the following URL: `https://atom.io/packages/ `! + +# Continuous Integration + +The final step is to ensure that your package will continue to work in the future when you or your contributors will add new features but also when Atom releases a new beta version. To do that, you can use [Travis-CI](https://travis-ci.org) on your repository with the following configuration: + +```yaml +language: objective-c + +notifications: + email: + on_success: never + on_failure: change + +script: 'curl -s https://raw.githubusercontent.com/nikhilkalige/docblockr/develop/spec/atom-build-package.sh | sh' + +env: + global: + - APM_TEST_PACKAGES="" + + matrix: + - ATOM_CHANNEL=stable + - ATOM_CHANNEL=beta +``` + +# Conclusion + +I personally think that this is a little revolution to allow developers to make their own editor and bring the features they want. + +Moreover, the Atom API is already very rich and very simple to use and this is certainly the main reason why the community offers a large number of packages. +To conclude, as for all libraries, it is not useful to reinvent the wheel by creating already existing packages. The idea is to add features if they don't already exists, in order to enrich your user experience. diff --git a/_posts/2017-01-17-redux-structurez-vos-applications-front.md b/_posts/2017-01-17-redux-structurez-vos-applications-front.md new file mode 100644 index 000000000..536f181be --- /dev/null +++ b/_posts/2017-01-17-redux-structurez-vos-applications-front.md @@ -0,0 +1,254 @@ +--- +layout: post +title: 'Redux : Structurez vos applications front' +permalink: /fr/redux-structurez-vos-applications-front/ +author: vcomposieux +date: '2017-01-17 10:09:00 +0100' +date_gmt: '2017-01-17 09:09:00 +0100' +categories: + - Javascript +tags: + - Javascript + - react + - redux + - vuejs +--- +L'écosystème Javascript est très riche, beaucoup de développeurs mais aussi de frameworks et d'outils sont disponibles. +Lorsque vous souhaitez développer une application, quel que soit son framework de rendu, vous allez vite être amené à vouloir architecturer votre projet afin de différencier et d'organiser les données des vues. C'est particulièrement le cas lorsque vous utilisez des frameworks de rendu de composants comme `React` ou `VueJS`. + +Historiquement, le besoin s'est fait sentir sur [React](https://facebook.github.io/react/) et Facebook a donc ouvert les sources de son outil [Flux](http://facebook.github.io/flux/). + +Le principe est le suivant : + + + +Votre application déclare, pour chaque composant, les `actions` qui lui sont liées. Ces actions permettent de définir l'état de votre composant, stocké dans un `store` , qui permet de maintenir votre `vue` à jour. +L'inconvénient est que dans ce cas, vous avez un store par composant. Ce modèle fonctionne pour React mais vous pouvez vous sentir limité sur certaines applications. +Dan Abramov a donc lancé, en juin 2015, [Redux](http://redux.js.org), qui permet principalement de simplifier la gestion du store car il y a en effet qu'un seul store pour toute votre application dans Redux. + +Tous vos composants peuvent donc accéder à vos données. + +Pour plus d'informations sur les différences Redux / Flux, je vous invite à lire cette [réponse de Dan](http://stackoverflow.com/questions/32461229/why-use-redux-over-facebook-flux/32920459#32920459). + +# Installation + +Nous allons voir dans cet article comment mettre en place et utiliser Redux sur vos projets. +Notez dès maintenant que la librairie peut être utilisée avec plusieurs librairies de rendu comme React ou VueJS. +Pour installer Redux, il vous faudra installer le package npm (ou yarn) `redux`. + +Si vous utilisez Redux sur une application React, il vous faudra également le package `react-redux` ou encore `vue-redux` s'il s'agit d'un projet VueJS. + +```bash +$ yarn add redux +``` + +Rien de plus, vous êtes prêt à utiliser Redux. + +# Utilisation classique + +Comme décrit précédemment, il vous faudra initialiser un `store` qui va permettre de stocker l'état de votre application. + +Pour instancier ce store, il vous faudra passer un ou plusieurs `reducers` . Les reducers contiennent les méthodes qui effectuent le changement d'état de votre application. + +Ces changements d'état sont effectués lorsqu'une `action` est déclenchée sur votre application. +Voilà, nous avons là les 3 composantes d'une application structurée par Redux : des `actions`, des `reducers` et un `store`. +Nous allons prendre un cas pratique simple : un compteur que l'on peut incrémenter ou décrémenter d'une certaine valeur. + +Voici l'arborescence que nous ciblons : + +``` +src/ +├── actions +│ └── counter.js +├── constants +│ └── ActionTypes.js +├── reducers +│ ├── another.js +│ ├── counter.js +│ └── index.js +└── store + └── configureStore.js +``` + +## Actions + +Écrivons donc un fichier d'actions qui permet de définir ces deux actions : incrémenter et décrémenter. +Avant tout, nous allons également stocker ces noms d'actions dans des constantes, ce qui nous permettra d'être clair dans notre code car nous ferons toujours appel à ces constantes. +Créez donc un fichier `src/constants/ActionTypes.js` avec le contenu : + +```js +export const INCREMENT = 'INCREMENT'; +export const DECREMENT = 'DECREMENT'; +``` + +Nous allons maintenant écrire les définitions des actions. Créez maintenant le fichier `src/actions/counter.js` : + +```js +import * as types from '../constants/ActionTypes'; + +export const increment = (value) => ({ type: types.INCREMENT, value }); +export const decrement = (value) => ({ type: types.DECREMENT, value }); +``` + +Vous venez de déclarer deux actions (`increment` et `decrement` ) qui prennent chacune un type (obligatoire) et une valeur à ajouter ou soustraire. + +## Reducers + +Il nous faut maintenant écrire les méthodes des reducers permettant de mettre à jour l'état de notre application. +Ces reducers seront écrits dans le fichier `src/reducers/counter.js` : + +```js +import { INCREMENT, DECREMENT } from '../constants/ActionTypes'; + +const initialState = { + current: 0, +}; + +export default function counter(state = initialState, action) { + switch (action.type) { + case INCREMENT: + return { + current: state.current += action.value, + }; + + case DECREMENT: + return { + current: state.current -= action.value, + }; + + default: + return state; + } +} +``` + +Vous avez compris l'idée, nous avons nos actions dans un `switch() { case ... }` et mettons directement à jour les valeurs de notre store. +Vous remarquerez que nous avons créés un état initial (initialState) afin d'initialiser les valeurs de notre application. + +`Note :` Il vous est possible de créer autant de reducers que nécessaire. + +Si vous avez déclaré plusieurs reducers dans votre application, vous pouvez les combiner dans un fichier `src/reducers/index.js` comme suit : + +```js +import { combineReducers } from 'redux'; + +import counter from './counter'; +import another from './another'; + +const reducers = combineReducers({ + counter, + another, +}); + +export default reducers; +``` + +## Store + +Maintenant que nous avons nos actions et reducers, dernière étape indispensable : la création du store ! + +Créez un fichier `src/store/configureStore.js` avec le contenu suivant : + +```js +import { createStore } from 'redux'; +import reducers from '../reducers'; + +const configureStore = () => { + return createStore( + reducers, + ); +}; + +export default configureStore; +``` + +Nous utilisons ici la fonction `createStore()` de l'API Redux permettant de créer notre store. + +Afin d'aller un peu plus loin, notez que cette fonction peut prendre jusqu'à 3 arguments : + +* un ou des reducers, +* un état pré-chargé (optionnels), correspondant à un état initial, +* des "enhancers" (optionnels), autrement dit des callbacks comme des middlewares. + +Un middleware permet d'exécuter une callback à chaque fois que le `dispatch()` d'actions est exécuté. + +Voici un exemple de middleware permettant de logger chaque action déclenchée : + +```js +import { createStore, applyMiddleware } from 'redux' +import reducers from '../reducers'; + +function logger({ getState }) { + return (next) => (action) => { + console.log('will dispatch', action) + return next(action) + } +} + +const configureStore = () => { + return createStore( + reducers, + applyMiddleware(logger) + ); +}; + +export default configureStore; +``` + +N'oubliez pas d'utiliser la fonction `applyMiddleware()` lorsque vous passez vos fonctions de middleware au store. + +# Utilisation avec React + +Le principe reste exactement le même lorsque Redux est utilisé avec React, cependant, la librairie `react-redux` va vous apporter des petites choses en plus. +Vous allez en effet pouvoir lier l'état de votre application gérée par Redux ainsi que les actions que vous avez définies avec les `props` de vos composants React. + +Prenons un composant `Counter` reflétant l'architecture Redux mise en place dans notre cas d'exemple : + +```js +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import * as CounterActions from '../actions/counter'; + +const Counter = ({ children, value, actions }) => ( + + + ++); + +Counter.propTypes = { + children: PropTypes.object.isRequired, + value: PropTypes.number.isRequired, + actions: PropTypes.object.isRequired, +}; + +const mapStateToProps = state => ({ + value: state.counter.current, +}); + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators(CounterActions, dispatch), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Counter); +``` + +De cette façon, nous récupérons donc les valeurs de nos props provenant de notre store mais également une propriété `actions` permettant d'appeler nos actions Redux. +Les principaux éléments à noter ici sont : + +* `mapStateToProps` est une fonction permettant de mapper des `valeurs de notre state` Redux avec des `propriétés React`, +* `mapDispatchToProps` est une fonction permettant de mapper des `actions` Redux avec des `propriétés React`. + +Ces deux fonctions sont ensuite appliquées à l'aide de la fonction `connect()` fournie par `react-redux`. + +`Note :` Nous devons ici utiliser `bindActionCreators()` sur nos `CounterActions` car il s'agit d'un objet dont les valeurs sont des actions et cette fonction va permettre d'ajouter un appel à la fonction `dispatch()` de Redux afin que celles-ci soient correctement déclenchées. + +# Conclusion + +Si nous mettons en parallèle les `1 303 720 téléchargements sur le mois précédent` `de la librairie Redux` avec les `2 334 221 de téléchargements pour React`, nous remarquons que Redux est aujourd'hui `très utilisé` et semble vraiment très `apprécié` par les développeurs car il s'agit d'une solution `simple` qui permet réellement de structurer une application front. +Redux apporte, selon moi, une `vraie solution` permettant de structurer des applications au métier complexe aux communautés comme React, VueJS mais également aux autres. diff --git a/_posts/2017-01-20-redux-structure-frontend-applications.md b/_posts/2017-01-20-redux-structure-frontend-applications.md new file mode 100644 index 000000000..c1127d148 --- /dev/null +++ b/_posts/2017-01-20-redux-structure-frontend-applications.md @@ -0,0 +1,254 @@ +--- +layout: post +title: 'Redux: Structure your frontend applications' +permalink: /en/redux-structure-frontend-applications/ +author: vcomposieux +date: '2017-01-20 12:12:34 +0100' +date_gmt: '2017-01-20 11:12:34 +0100' +categories: + - Javascript +tags: + - Facebook + - Javascript + - react + - redux +--- +Javascript ecosystem is really rich: full of developers but also full of frameworks and libraries. + +When you want to develop a frontend application, whatever its rendering framework, you will have to structure things into your project in order to organize the data management with views. This case occurs particularly when you use component rendering frameworks like `React` or `VueJS`. +Historically, this has been needed by [React](https://facebook.github.io/react/) so that's why Facebook has open sourced its tool named [Flux](http://facebook.github.io/flux/). + +Here is the philosophy: + + + +Your application declare `actions` for each components. These actions allow you to define the state of your data which is stored in a `store` . This stores continually maintains your `view` up-to-date. +We have a drawback in this case because you have to define one store per component. This is working but on large applications you can feel limited with it. +In June 2015, Dan Abramov has launched [Redux](http://redux.js.org/) which simplify store management because you only have one store for all your application. + +All of your application components can access to the whole state. + +For more information about Redux/Flux differences I encourage you to have a look at [Dan's answer](http://stackoverflow.com/questions/32461229/why-use-redux-over-facebook-flux/32920459#32920459) on this subject. + +# Installation + +This article will deal about how to install and use Redux on your own projects. +Please keep in mind that Redux can be used with multiple rendering frameworks like React or VueJS. +To install Redux, you will just need the `redux` npm (or yarn) package. + +If you use Redux into a React application, you will also need the `react-redux` package or even the `vue-redux` if you want to use it on a VueJS project. + +```bash +$ yarn add redux +``` + +Nothing more, you can now start to use Redux. + +# Basic usage + +As previously described, you will have to instanciate a new `store` that will allow to store the state of all your application. +In order to instanciate this store, you will have to give to it some `reducers` . Reducers contain methods that change the state of your application. +These state changes occur when an `action` is dispatched by your application. + +Here we are, we have the 3 things needed by a Redux application: `actions`, `reducers` and a `store`. +We will use a simple practical case: a counter that we can increment or decrement with a given value. + +Here is our target arborescence: + +``` +src/ +├── actions +│ └── counter.js +├── constants +│ └── ActionTypes.js +├── reducers +│ ├── another.js +│ ├── counter.js +│ └── index.js +└── store + └── configureStore.js +``` + +## Actions + +Let's write an actions containing file that will implement our 2 actions: increment and decrement. +Before all, we will store these actions names into constants in order to keep our code clear and comprehensible as we will always call these constants in all of our code. + +Start by creating a `src/constants/ActionTypes.js` file with the following content: + +```js +export const INCREMENT = 'INCREMENT'; +export const DECREMENT = 'DECREMENT'; +``` + +Great, we will now write actions that correspond to these constants in a `src/actions/counter.js` file: + +```js +import * as types from '../constants/ActionTypes'; + +export const increment = (value) => ({ type: types.INCREMENT, value }); +export const decrement = (value) => ({ type: types.DECREMENT, value }); +``` + +You have just created your 2 actions (`increment` and `decrement`) which each have a type property (required) and a value to add or remove to the current counter value. + +## Reducers + +We will now write reducers functions that correspond to the actions we previously wrote in order to update the value in our application state. + +This will be written in the `src/reducers/counter.js` file: + +```js +import { INCREMENT, DECREMENT } from '../constants/ActionTypes'; + +const initialState = { + current: 0, +}; + +export default function counter(state = initialState, action) { + switch (action.type) { + case INCREMENT: + return { + current: state.current += action.value, + }; + + case DECREMENT: + return { + current: state.current -= action.value, + }; + + default: + return state; + } +} +``` + +You got the idea, we have our actions wrapped into a `switch() { case ... }` and directly return the store updated with new values. +You can also observe that we have initialized an initial state (initialState) in order to prepare our application state with some default values. + +`Note:` You can write as many reducers as you need in your application so you can clearly split your code application. + +Only point if you declare multiple reducers into your application is that you will have to combine them here in a file named `src/reducers/index.js` as follows: + +```js +import { combineReducers } from 'redux'; + +import counter from './counter'; +import another from './another'; + +const reducers = combineReducers({ + counter, + another, +}); + +export default reducers; +``` + +## Store + +You have your actions and your reducers so let's dive into the final step: store creation! +Store will be created in a `src/store/configureStore.js` file with only these couple of lines: + +```js +import { createStore } from 'redux'; +import reducers from '../reducers'; + +const configureStore = () => { + return createStore( + reducers, + ); +}; + +export default configureStore; +``` + +You just have to call the Redux's `createStore()` API function in order to create your store. +In order to go further, please note that this function can take a maximum of 3 arguments: + +* one or many combines reducers, +* a pre-loaded state (*optional*), corresponding to an initial state, +* some "enhancers" (*optionals*), which are some callbacks such as middlewares. + +A middleware is a callback that is executed each time Redux can the `dispatch()` function so each time an action is triggered. + +Here is a simple middleware that logs each dispatched actions: + +```js +import { createStore, applyMiddleware } from 'redux' +import reducers from '../reducers'; + +function logger({ getState }) { + return (next) => (action) => { + console.log('will dispatch', action) + return next(action) + } +} + +const configureStore = () => { + return createStore( + reducers, + applyMiddleware(logger) + ); +}; + +export default configureStore; +``` + +Do not forget to call the `applyMiddleware()` function when you pass your function to the store argument. + +# React use case + +Principles are exactly the same when you want to use Redux on a React application. However, the `react-redux` library brings some cool additional features to fit with React. +Indeed, thanks to this library, you will be able to map your React components `props` with the Redux state and actions. + +Let's take a concrete case: a `Counter` component which could be a component for our previous use case: + +```js +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import * as CounterActions from '../actions/counter'; + +const Counter = ({ children, value, actions }) => ( ++ + ++); + +Counter.propTypes = { + children: PropTypes.object.isRequired, + value: PropTypes.number.isRequired, + actions: PropTypes.object.isRequired, +}; + +const mapStateToProps = state => ({ + value: state.counter.current, +}); + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators(CounterActions, dispatch), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Counter); +``` + +This way, we are able to retrieve our props values which came from the Redux store but also an `actions` property that will allow us to dispatch Redux events when we will call it. + +Main things to note here are: + +* `mapStateToProps` is a function that allows to map our Redux **state properties** with `React properties`, +* `mapDispatchToProps` is a function that allows to map Redux `actions` with `React properties`. + +These two functions are applied thanks to the `connect()` function brought by the `react-redux` library. + +`Note:` We have to use the `bindActionCreators()` function over our `CounterActions` because this is an object that contains actions functions so this function will allows React to call the `dispatch()` Redux function when React will call the functions in order to have them correctly triggered. + +# Conclusion + +If we put in parallel the download numbers of Redux (`1 303 720 download over the previous month)` with the `2 334 221 downloads of React`, we can conclude that Redux is today `very used` and seems very much `appreciated` by developers because it's a `simple` solution that can greatly help you to structure your application. +Redux brings, in my opinion, a `real solution` to structure complex (or large) business applications and bring that to the React and VueJS (and others) communities. diff --git a/_posts/2017-02-22-consul-service-discovery-failure-detection-2.md b/_posts/2017-02-22-consul-service-discovery-failure-detection-2.md new file mode 100644 index 000000000..fbf4ec8c7 --- /dev/null +++ b/_posts/2017-02-22-consul-service-discovery-failure-detection-2.md @@ -0,0 +1,285 @@ +--- +layout: post +title: 'Consul : Service Discovery et Failure Detection' +permalink: /fr/consul-service-discovery-failure-detection-2/ +author: vcomposieux +date: '2017-02-22 10:49:25 +0100' +date_gmt: '2017-02-22 09:49:25 +0100' +categories: + - Dev Ops +tags: + - service + - consul + - discovery + - failure + - detection + - health + - check +--- +# Introduction + +Consul est un outil développé en Go par la société HashiCorp et a vu le jour en 2013. +Consul a plusieurs composants mais son objectif principal est de regrouper la connaissance des services d'une architecture (service discovery) et permet de s'assurer que les services contactés sont toujours disponibles en s'assurant que la santé de ces services est toujours bonne (via du health check). + +Concrètement, Consul va nous apporter un serveur DNS permettant de mettre à jour les adresses IP disponibles pour un service, en fonction de ceux qui sont en bonne santé. Ceci permet également de faire du load balancing bien que nous verrons qu'il ne permette pas pour le moment de préférer un service à un autre. +Il offre également d'autres services tel que du stockage clé/valeur, nous l'utiliserons dans cet article afin que Docker Swarm y stocke ses valeurs. + +Afin de clarifier la suite de cet article, voici les ports utilisés par Consul : + +* `8300` (+ `8301` et `8302`) : Echanges via RPC, +* `8400` : Echanges via RPC par le CLI, +* `8500` : Utilisé pour l'API HTTP et l'interface web, +* `8600` : Utilisé pour le serveur DNS. + +La suite de cet article va se concentrer sur la partie service discovery et failure detection. Nous allons pour cela mettre en place un cluster Docker Swarm possédant l'architecture suivante : + + + +Nous aurons donc 3 machines Docker : + +* Une machine avec `Consul` (Swarm Discovery), +* Une machine étant notre "`node 01`" avec un service HTTP (Swarm), +* Une machine étant notre "`node 02`" avec un service HTTP (Swarm). + +Nous mettrons également sur nos deux nodes (cluster Docker Swarm) un container Docker pour Registrator, permettant de faciliter l'enregistrement de nos services Docker sur Consul. + +Pour plus d'informations concernant `Registrator`, vous pouvez vous rendre sur : [https://gliderlabs.com/registrator/](https://gliderlabs.com/registrator/) +Commençons à installer notre architecture ! + +# Service discovery + +## Première machine : Consul (Swarm Discovery) + +Nous allons commencer par créer la première machine : notre Consul. + +Pour cela, tapez : + +```bash +$ docker-machine create -d virtualbox consul +``` + +Une fois la machine prête, préparez votre environnement pour utiliser cette machine et lancez un container Consul : + +```bash +$ eval $(docker-machine env consul) +$ docker run -d \ + -p 8301:8301 \ + -p 8302:8302 \ + -p 8400:8400 \ + -p 8500:8500 \ + -p 53:8600/udp \ + consul +``` + +Nous avons maintenant notre Consul prêt à recevoir nos services et nos prochaines machines membres de notre cluster Docker Swarm. + +Vous pouvez d'ailleurs ouvrir l'interface web mise à disposition en obtenant l'ip de la machine Consul : + +```bash +$ docker-machine ip consul ++``` + +Puis ouvrez dans votre navigateur l'URL : `http:// :8500`. + +## Deuxième machine : Node 01 + +Nous allons maintenant créer la machine correspondant au premier node de notre cluster Docker Swarm qui se verra également obtenir le rôle de master de notre cluster Swarm (il en faut bien un). + +```bash +$ docker-machine create -d virtualbox \ + --swarm \ + --swarm-master \ + --swarm-discovery="consul://$(docker-machine ip consul):8500" \ + --engine-opt="cluster-store=consul://$(docker-machine ip consul):8500" \ + --engine-opt="cluster-advertise=eth1:2376" swarm-node-01 +``` + +Comme vous le voyez, nous précisons l'option `--swarm-discovery` avec l'IP de notre machine Consul et le port 8500 correspondant à l'API de Consul. Ainsi, Docker Swarm pourra utiliser l'API pour enregistrer les machines du cluster. + +Nous allons maintenant configurer notre environnement pour utiliser cette machine et y installer dessus un container Registrator permettant d'auto-enregistrer les nouveaux services sur Consul. + +Pour ce faire, tapez : + +```bash +$ eval $(docker-machine env swarm-node-01)