<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adamo Crespi</title>
    <description>The latest articles on DEV Community by Adamo Crespi (@aerendir).</description>
    <link>https://dev.to/aerendir</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F419549%2F069fe111-3977-43ba-866d-bd527f88464e.jpeg</url>
      <title>DEV Community: Adamo Crespi</title>
      <link>https://dev.to/aerendir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aerendir"/>
    <language>en</language>
    <item>
      <title>How to debug ANY Symfony command simply passing `-x`</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Thu, 04 Apr 2024 16:46:10 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-debug-any-symfony-command-simply-passing-x-214o</link>
      <guid>https://dev.to/serendipityhq/how-to-debug-any-symfony-command-simply-passing-x-214o</guid>
      <description>&lt;p&gt;Debugging a Symfony console command requires setting some environment variables (depending on your actual configuration of xDebug):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;XDEBUG_SESSION=1&lt;/code&gt; (&lt;a href="https://xdebug.org/docs/step_debug#activate-debugger-cmd" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XDEBUG_MODE=debug&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XDEBUG_ACTIVATED=1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check the purpose of these environment variables on the &lt;a href="https://xdebug.org/docs/all_settings" rel="noopener noreferrer"&gt;xDebug's Docs&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  TL;DR
&lt;/h1&gt;

&lt;p&gt;So, launching a Symfony console command in debug mode would look like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;

XDEBUG_SESSION=1 XDEBUG_MODE=debug XDEBUG_ACTIVATED=1 php bin/console my:command --an-option --an argument


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Using the listener below, instead, you can debug any Symfony command this way:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;

bin/console my:command --an-option --an argument -x


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Really shorter and faster, also to debug a command on the fly, without having to move the cursor to the beginning of the command (that is so boring to do on the CLI! 🤬).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-x&lt;/code&gt; option starts the "magic" and the listener actually performs the trick.&lt;/p&gt;

&lt;p&gt;Using this listener, you can actually debug ANY Symfony command, also if it doesn't belong to your app, but belongs to, for example, Doctrine, a third party bundle or even Symfony itself.&lt;/p&gt;

&lt;p&gt;It's really like magic!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnc4h3onhognc8aktz99g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnc4h3onhognc8aktz99g.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  WARNING
&lt;/h2&gt;

&lt;p&gt;The command will not work to debug the Symfony's container building: in that phase, in fact, the listeners are not still active.&lt;/p&gt;

&lt;p&gt;If you need to debug the configuration of a bundle or any other part of Symfony that is before the container is built and services configured, then you still need to go the old way, with full declaration of env variables directly in the command call in the command line.&lt;/p&gt;

&lt;h1&gt;
  
  
  The &lt;code&gt;RunCommandInDebugModeEventListener&lt;/code&gt; listener
&lt;/h1&gt;

&lt;p&gt;The listener works thanks to the &lt;a href="https://symfony.com/doc/current/components/console/events.html" rel="noopener noreferrer"&gt;&lt;code&gt;ConsoleEvents::COMMAND&lt;/code&gt; event&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It simply searches for the flag &lt;code&gt;-x&lt;/code&gt; (or &lt;code&gt;--xdebug&lt;/code&gt;) and, if it finds it, then restarts the command setting the environment variables required by xDebug to work.&lt;/p&gt;

&lt;p&gt;The restart of the command is done through the &lt;a href="https://www.php.net/manual/en/function.passthru.php" rel="noopener noreferrer"&gt;PHP function &lt;code&gt;passthru()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The rest of the code is sufficiently self explanatory, so I'm not going to explain it.&lt;/p&gt;

&lt;p&gt;This is the listener: happy Symfony commands debugging!&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;

&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\EventListener&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Console\Command\Command&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Console\Command\HelpCommand&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Console\ConsoleEvents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Console\Event\ConsoleCommandEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Console\Input\ArgvInput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Console\Input\InputOption&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\EventDispatcher\Attribute\AsEventListener&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[AsEventListener(ConsoleEvents::COMMAND, 'configure')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RunCommandInDebugModeEventListener&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ConsoleCommandEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCommand&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The command must be an instance of '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;HelpCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getActualCommandFromHelpCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'xdebug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;shortcut&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InputOption&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VALUE_NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'If passed, the command is re-run setting env variables required by xDebug.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;HelpCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInput&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;ArgvInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isInDebugMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'XDEBUG_SESSION'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;writeln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;comment&amp;gt;Relaunching the command with xDebug...&amp;lt;/comment&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;buildCommandWithXDebugActivated&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nf"&gt;\passthru&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getActualCommandFromHelpCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;HelpCommand&lt;/span&gt; &lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Command&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$reflection&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\ReflectionClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$property&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$reflection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'command'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$actualCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$property&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$actualCommand&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The command must be an instance of '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$actualCommand&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isInDebugMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ArgvInput&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTokensFromArgvInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tokens&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--xdebug'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'-x'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * @return array&amp;lt;string&amp;gt;
     */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getTokensFromArgvInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ArgvInput&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$reflection&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\ReflectionClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$tokensProperty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$reflection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tokens'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$tokens&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$tokensProperty&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nb"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tokens&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Impossible to get the arguments and options from the command.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$tokens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;buildCommandWithXDebugActivated&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$serverArgv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'argv'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$serverArgv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Impossible to get the arguments and options from the command: the command cannot be relaunched with xDebug.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'SCRIPT_NAME'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Impossible to get the name of the command: the command cannot be relaunched with xDebug.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$phpBinary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;PHP_BINARY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$args&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serverArgv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"XDEBUG_SESSION=1 XDEBUG_MODE=debug XDEBUG_ACTIVATED=1 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$phpBinary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$script&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>symfony</category>
      <category>xdebug</category>
      <category>php</category>
      <category>cli</category>
    </item>
    <item>
      <title>Moving Symfony files in the Docker container</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Thu, 22 Nov 2018 11:22:48 +0000</pubDate>
      <link>https://dev.to/serendipityhq/moving-symfony-files-in-the-docker-container-31dh</link>
      <guid>https://dev.to/serendipityhq/moving-symfony-files-in-the-docker-container-31dh</guid>
      <description>&lt;p&gt;Once you have created the &lt;a href="https://dev.to/aerendir/how-to-dockerize-a-symfony-application-introduction-and-the-web-server-3303-temp-slug-4506360"&gt;first configuration of your basic Docker stack&lt;/a&gt;, it is time to move the Symfony’s files into Docker’s web server service.&lt;/p&gt;

&lt;p&gt;This will make us able to run the symfony application from inside the web server container, getting the real benefits of using a system as Docker (same environment on any machine, above all).&lt;/p&gt;

&lt;p&gt;So, we need a way to move in the container all the files required to make our project run.&lt;/p&gt;

&lt;p&gt;How do we do this?&lt;/p&gt;

&lt;p&gt;The answer to our question is a really simple word: “Dockerfile” (never heard of it? Ok, it is a not so simple word! But it is simple to use it anyway! &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt; )&lt;/p&gt;

&lt;p&gt;From the Docker documentation &lt;a href="https://docs.docker.com/engine/reference/builder/"&gt;about “Dockerfile”&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Docker can build images automatically by reading the instructions from a Dockerfile&lt;/p&gt;

&lt;p&gt;A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.&lt;/p&gt;

&lt;p&gt;Using docker build users can create an automated build that executes several command-line instructions in succession.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, basically, all what you write in the console to configure your environment has to be put in a Dockerfile to automate the process.&lt;/p&gt;

&lt;p&gt;Anytime someone wants to configure the environment to run our app, (s)he will not have to take care of the details as they are all in the Dockerfile: easy and fast!&lt;/p&gt;

&lt;p&gt;So, let’s prepare our app to use Dockerfiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the folder structure and the &lt;code&gt;Dockerfile&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;First of all we need to create some folders.&lt;/p&gt;

&lt;p&gt;So, in the root of your app, create a folder called &lt;code&gt;docker&lt;/code&gt;, inside it create another folder called &lt;code&gt;build&lt;/code&gt; and inside it another folder called &lt;code&gt;apache&lt;/code&gt;, then inside it create a file called &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final folder structure will be like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5ixpCxoY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-dockerfile-folder-structure.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5ixpCxoY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-dockerfile-folder-structure.png" alt="" width="214" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next steps are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put in the &lt;code&gt;Dockerfile&lt;/code&gt; the commands required to build the &lt;code&gt;apache&lt;/code&gt; container;&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;docker-compose.yaml&lt;/code&gt; to use this new &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As first thing, instruct the &lt;code&gt;Dockerfile&lt;/code&gt; about the Apache image we want to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# docker/build/apache/Dockerfile

FROM httpd:2.4

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;httpd:2.4&lt;/code&gt; is exactly the same we used in our &lt;code&gt;docker-compose.yaml&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Now, edit the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file so it will use this &lt;code&gt;Dockerfile&lt;/code&gt;: we will do this using the &lt;a href="https://docs.docker.com/compose/compose-file/#build"&gt;&lt;code&gt;build&lt;/code&gt;&lt;/a&gt; key node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
  # "php" was "language" in previous example
  ...

  # Configure the database
  ...

  # Configure Apache
  apache:
    # image: httpd:2.4 &amp;lt;- Remove this node
    build: docker/build/apache
    ports:
      - "8100:80"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once edited the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file, first run &lt;code&gt;docker-compose down&lt;/code&gt; to stop all the containers, then run &lt;code&gt;docker-compose build&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose down
Stopping app-aragog-www_mysql_1 ... done
Stopping app-aragog-www_apache_1 ... done
Removing app-aragog-www_php_1 ... done
Removing app-aragog-www_mysql_1 ... done
Removing app-aragog-www_apache_1 ... done
Removing network app-aragog-www_default
MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose build
php uses an image, skipping
mysql uses an image, skipping
Building apache
Step 1/1 : FROM httpd:2.4
 ---&amp;gt; fb2f3851a971

Successfully built fb2f3851a971
Successfully tagged app-aragog-www_apache:latest

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the &lt;code&gt;php&lt;/code&gt; and &lt;code&gt;mysql&lt;/code&gt; containers are skipped as they use an image, while the &lt;code&gt;apache&lt;/code&gt; container is built as it uses a &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Try to start the containers with &lt;code&gt;docker-compose up -d&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose up -d
Creating network "app-aragog-www_default" with the default driver
Creating app-aragog-www_mysql_1 ... done
Creating app-aragog-www_php_1 ... done
Creating app-aragog-www_apache_1 ... done

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And access the URL &lt;code&gt;http://127.0.0.1:8100&lt;/code&gt;: you see the message “It works!”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ke283tum--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-port-binding.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ke283tum--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-port-binding.png" alt="" width="425" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok, the first step is done: the Apache web server is continuing to work also if we are using a different method to configure it.&lt;/p&gt;

&lt;p&gt;Now we need to do the second step: add the Symfony files to the document root of Apache.&lt;/p&gt;

&lt;p&gt;To do this, we first need to understand what is the Docker Context as it will be extremely useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Docker Context: what is it, why we need to understand it and how to use it
&lt;/h2&gt;

&lt;p&gt;Before diving in the files and their copy in the container, we need to understand what is Docker Context.&lt;/p&gt;

&lt;p&gt;From the documentation about &lt;a href="https://docs.docker.com/engine/reference/commandline/build/#extended-description"&gt;&lt;code&gt;build&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;docker build&lt;/code&gt; command builds Docker images from a Dockerfile and a “context”.&lt;/p&gt;

&lt;p&gt;A build’s context is the set of files located in the specified &lt;code&gt;PATH&lt;/code&gt; or &lt;code&gt;URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The build process can refer to any of the files in the context.&lt;/p&gt;

&lt;p&gt;For example, your build can use a &lt;code&gt;COPY&lt;/code&gt; instruction to reference a file in the context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, to copy our Symfony files to the container, we need to make them available in the context.&lt;/p&gt;

&lt;p&gt;We are using a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file, so we need to understand how to specify the context when using it.&lt;/p&gt;

&lt;p&gt;The solution? The &lt;a href="https://docs.docker.com/compose/compose-file/#context"&gt;&lt;code&gt;context&lt;/code&gt; key of the &lt;code&gt;build&lt;/code&gt; node&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The documentation says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Either a path to a directory containing a Dockerfile, or a url to a git repository.&lt;/p&gt;

&lt;p&gt;When the value supplied is a relative path, it is interpreted as relative to the location of the Compose file.&lt;/p&gt;

&lt;p&gt;This directory is also the build context that is sent to the Docker daemon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But there is more: we can also set a &lt;a href="https://docs.docker.com/compose/compose-file/#dockerfile"&gt;specific &lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; to use when setting the context.&lt;/p&gt;

&lt;p&gt;This is useful as &lt;a href="https://docs.docker.com/engine/reference/commandline/build/#text-files"&gt;because&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By default the docker build command will look for a Dockerfile at the root of the build context&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, trying to recap what we have read until now in the documentation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We can copy or add files in the container from our machine only if they are in the “context”;&lt;/li&gt;
&lt;li&gt;Using the &lt;code&gt;build&lt;/code&gt; key in the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file with a path (&lt;code&gt;build: path/to/folder&lt;/code&gt;):

&lt;ol&gt;
&lt;li&gt;Uses the &lt;code&gt;Dockerfile&lt;/code&gt; found in this path;&lt;/li&gt;
&lt;li&gt;Uses the content of this path as “context” (so we can access only files in it)&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;In our &lt;code&gt;docker-compose.yaml&lt;/code&gt; file, we can:

&lt;ol&gt;
&lt;li&gt;Specify a specific “context” to use to build the service/container;&lt;/li&gt;
&lt;li&gt;Specify a specific &lt;code&gt;Dockerfile&lt;/code&gt; to use to build the service/container.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Interesting, isn’t it? Let’s put all those information at work to understand how to copy our Symfony files to the Apache container.&lt;/p&gt;

&lt;p&gt;As usual, we will start small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying to copy only a single file &lt;code&gt;test.txt&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;First, we need a file to copy: create an empty file called &lt;code&gt;test.txt&lt;/code&gt; and put it in the root directory of the project: we will try to copy it in the container.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-copy-test-from-context.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1LUeeeos--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-copy-test-from-context-198x300.png" alt="" width="198" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s edit our &lt;code&gt;docker-compose.yaml&lt;/code&gt; file to specify the desired context and &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# docker-compose.yaml

version: '3.7'
services:
  # "php"
  ...

  # Configure the database
  ...

  # Configure Apache
  apache:
    # build: docker/build/apache &amp;lt;- Remove this node
    # And recreate it as follows
    build:
      context: .
      dockerfile: docker/build/apache/Dockerfile
    ports:
      - "8100:80"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we removed the path to the &lt;code&gt;apache&lt;/code&gt; folder and, instead, added two more keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;context&lt;/code&gt;, that sets the context to the current folder (that is the root folder, as the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file is in the root);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dockerfile&lt;/code&gt;, that specifies which &lt;code&gt;Dockerfile&lt;/code&gt; to use to build the container/service that runs Apache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This has two effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sets the Docker Context to our root folder as the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file is in the root (so we can now use – so, we can copy, too – any file in our root folder);&lt;/li&gt;
&lt;li&gt;Explicitly specifies the &lt;code&gt;Dockerfile&lt;/code&gt; to use, without relying on the auto-discovery capabilities of Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before this modification, instead, the context was the folder &lt;code&gt;docker/build/apache&lt;/code&gt; (and so we were able to copy only files and folders in that folder) and the &lt;code&gt;Dockerfile&lt;/code&gt; was found thanks to the auto-discovery capabilities of Docker in finding &lt;code&gt;Dockerfile&lt;/code&gt;s in the provided paths.&lt;/p&gt;

&lt;p&gt;Now that we have our entire root folder in the Docker Context, edit the &lt;code&gt;Dockerfile&lt;/code&gt; that builds Apache to make it able to copy the file &lt;code&gt;test.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# docker/build/apache/Dockerfile

FROM httpd:2.4

# Copy Symfony files: COPY [FROM_MACHINE] [TO_CONTAINER]
COPY test.txt copied-test.txt

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are using the &lt;a href="https://docs.docker.com/engine/reference/builder/#copy"&gt;&lt;code&gt;COPY&lt;/code&gt;&lt;/a&gt; instruction.&lt;/p&gt;

&lt;p&gt;As the context is our root folder, we simply use the file name &lt;code&gt;test.txt&lt;/code&gt; and copy it to the container as &lt;code&gt;copied-test.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now run the command &lt;code&gt;docker-compose build&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose build
php uses an image, skipping
mysql uses an image, skipping
Building apache
Step 1/2 : FROM httpd:2.4
 ---&amp;gt; fb2f3851a971
Step 2/2 : COPY test.txt copied-test.txt
 ---&amp;gt; 287d3e3b6a04

Successfully built 287d3e3b6a04
Successfully tagged app-aragog-www_apache:latest

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As usual, &lt;code&gt;php&lt;/code&gt; and &lt;code&gt;mysql&lt;/code&gt; are skipped, BUT &lt;code&gt;apache&lt;/code&gt; has now two steps, and the second one is exactly the &lt;code&gt;COPY&lt;/code&gt; instruction.&lt;/p&gt;

&lt;p&gt;Let’s see if the file was really copied.&lt;/p&gt;

&lt;p&gt;First, recreate the containers running the &lt;code&gt;up&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose up -d
Recreating app-aragog-www_apache_1 ... done
Starting app-aragog-www_php_1 ... done

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enter the container (use &lt;code&gt;docker ps&lt;/code&gt; to find the ID of the &lt;code&gt;apache&lt;/code&gt; container) and list the files in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker exec -it e4ed20c531ae bash
root@e4ed20c531ae:/usr/local/apache2# ls
bin build cgi-bin conf copied-test.txt error htdocs icons include logs modules

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BINGO! Our &lt;code&gt;test.txt&lt;/code&gt; file is there with the name &lt;code&gt;copied-test.txt&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;Now that we can use the &lt;code&gt;context&lt;/code&gt; to copy files in the container from it, we are ready to further explore the copying of Symfony files to the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving Symfony’s files into the container
&lt;/h2&gt;

&lt;p&gt;Now that we know how to move files to the container, we are ready to move our entire app to the container.&lt;/p&gt;

&lt;p&gt;We can use a simple instruction like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COPY . ./htdocs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will move the entire context (that is our entire root folder) in the creating container.&lt;/p&gt;

&lt;p&gt;WARNING: This is not the best way of doing this. For the moment we make simple things, so it is ok to copy the entire root folder.&lt;br&gt;&lt;br&gt;
Later in this series of posts, we will learn how to refine the files copied in the container to speed up and optimize the process.&lt;/p&gt;

&lt;p&gt;For the moment, let’s try this quick and dirty solution.&lt;/p&gt;

&lt;p&gt;Edit the &lt;code&gt;docker/build/apache/Dockerfile&lt;/code&gt;, adding the above instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM httpd:2.4

# Copy Symfony files
# COPY test.txt copied-test.txt &amp;lt;- Remove this line (and also the text.txt file!)

COPY . ./htdocs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild the containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose build
php uses an image, skipping
mysql uses an image, skipping
Building apache
Step 1/2 : FROM httpd:2.4
 ---&amp;gt; fb2f3851a971
Step 2/2 : COPY . ./htdocs
 ---&amp;gt; 3f3528537fe9

Successfully built 3f3528537fe9
Successfully tagged app-aragog-www_apache:latest

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the running ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose up -d
Recreating app-aragog-www_apache_1 ... 
Recreating app-aragog-www_apache_1 ... done
Starting app-aragog-www_php_1 ... done

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now enter the container and check if the files are in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8b10afca67dd app-aragog-www_apache "httpd-foreground" 8 seconds ago Up 4 seconds 8100-&amp;gt;80/tcp app-aragog-www_apache_1
c633398a7d65 mysql:5.7 "docker-entrypoint.s…" 15 hours ago Up 15 hours 3306/tcp app-aragog-www_mysql_1
MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker exec -it 8b10afca67dd bash
root@8b10afca67dd:/usr/local/apache2# ls
Docker.md README.md bin build cgi-bin composer.json composer.lock conf config docker docker-compose.yaml docs error htdocs icons include logs modules public src symfony.lock var vendor

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Are they there? Very good! We have just moved all the Symfony’s files in the container! &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s try to open our Symfony app: go to &lt;code&gt;http://127.0.0.1:8100&lt;/code&gt;…&lt;/p&gt;

&lt;p&gt;The message “It works!”? What’s happening?&lt;/p&gt;

&lt;p&gt;Ok, try this: &lt;code&gt;http://127.0.0.1:8100/public&lt;/code&gt;… A list of files? ? And there is our &lt;code&gt;index.php&lt;/code&gt; listed ?&lt;/p&gt;

&lt;p&gt;Click on it…&lt;/p&gt;

&lt;p&gt;What are you seeing? What, the content of the &lt;code&gt;php&lt;/code&gt; file, not interpreted but instead served as a simple text file? ?&lt;/p&gt;

&lt;p&gt;What is damn happening? ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Apache interpret our PHP files
&lt;/h2&gt;

&lt;p&gt;Technically, we have just created our stack and it works because we are able to make up and running our containers with Apache and MySQL (PHP currently doesn’t work, in fact you have never seen it when running &lt;code&gt;docker ps&lt;/code&gt;!).&lt;/p&gt;

&lt;p&gt;And it works because we can see the Symfony’s &lt;code&gt;index.php&lt;/code&gt; content.&lt;/p&gt;

&lt;p&gt;So, technically it is working, practically it isn’t as we are not able to serve the php files as interpreted webpages.&lt;/p&gt;

&lt;p&gt;Why does this happen?&lt;/p&gt;

&lt;p&gt;Let’s recap our current configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
  # "php" was "language" in previous example
  php:
    image: php:7.2

  # Configure the database
  mysql:
    image: mysql:5.7
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root}

  # Configure Apache
  apache:
    build:
      context: .
      dockerfile: docker/build/apache/Dockerfile
    ports:
      - "8100:80"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see we are building three containers: one for &lt;code&gt;php&lt;/code&gt;, one for &lt;code&gt;mysql&lt;/code&gt; and one for &lt;code&gt;apache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Leaving apart for the moment the container for &lt;code&gt;mysql&lt;/code&gt;, let’s examine deeper what we are doing with &lt;code&gt;php&lt;/code&gt; and &lt;code&gt;apache&lt;/code&gt; containers.&lt;/p&gt;

&lt;p&gt;We already know that the &lt;code&gt;php&lt;/code&gt; container doesn’t work and never starts nor is built: maybe it is time to understand why!&lt;/p&gt;

&lt;p&gt;To build &lt;code&gt;php&lt;/code&gt; and &lt;code&gt;apache&lt;/code&gt; we are using two images:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP: &lt;code&gt;php:7.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Apache: &lt;code&gt;httpd:2.4&lt;/code&gt; (this is in the Dockerfile in &lt;code&gt;docker/build/apache&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, the question at this point is: what happens when we build these images?&lt;/p&gt;

&lt;p&gt;The first thing we know is that the &lt;code&gt;php&lt;/code&gt; container is skipped. When running &lt;code&gt;docker-compose build&lt;/code&gt;, in fact, we can read a clear message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php uses an image, skipping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second thing we know is that the &lt;code&gt;php&lt;/code&gt; container is never run: using &lt;code&gt;docker ps&lt;/code&gt;, in fact, we have never seen it in the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d109c4b4c431 app-aragog-www_apache "httpd-foreground" 11 minutes ago Up 11 minutes 8100-&amp;gt;80/tcp app-aragog-www_apache_1
844167c3154f mysql:5.7 "docker-entrypoint.s…" 12 minutes ago Up 12 minutes 3306/tcp app-aragog-www_mysql_1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, in the end, we have this situation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We have the Apache container running, but without PHP;&lt;/li&gt;
&lt;li&gt;We have the PHP container NOT running, and we anyway don’t have PHP (but, as seen in the previous post about &lt;a href="https://dev.to/aerendir/how-to-dockerize-a-symfony-application-introduction-and-the-web-server-3303-temp-slug-4506360"&gt;how to configure the Apache web server on Docker&lt;/a&gt;, if we try to check the version of PHP, we get it, so it seems it is working: very confusing!).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Let’s see what these images do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Docker images (and containers)
&lt;/h2&gt;

&lt;p&gt;A Docker image is nothing more than a Dockerfile with some instructions to build a container.&lt;/p&gt;

&lt;p&gt;More &lt;a href="https://stackoverflow.com/a/23736802/1399706"&gt;precisely&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An instance of an image is called a container. You have an image, which is a set of layers as you describe. If you start this image, you have a running container of this image. You can have many running containers of the same image.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can read more about images and layers &lt;a href="https://docs.docker.com/v17.09/engine/userguide/storagedriver/imagesandcontainers/#images-and-layers"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The relvant part is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A Docker image is built up from a series of layers. Each layer represents an instruction in the image’s Dockerfile. Each layer except the very last one is read-only.&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;Each layer is only a set of differences from the layer before it. The layers are stacked on top of each other. When you create a new container, you add a new writable layer on top of the underlying layers. This layer is often called the “container layer”. All changes made to the running container, such as writing new files, modifying existing files, and deleting files, are written to this thin writable container layer. The diagram below shows a container based on the Ubuntu 15.04 image.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And this is the representation of what you’ve read until now:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DVbnP-Rp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://docs.docker.com/v17.09/engine/userguide/storagedriver/images/container-layers.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DVbnP-Rp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://docs.docker.com/v17.09/engine/userguide/storagedriver/images/container-layers.jpg" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But from where these images come?&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://hub.docker.com/"&gt;Docker Hub&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;So, to understand why our &lt;code&gt;httpd&lt;/code&gt; image doesn’t interpret correctly PHP files and why our &lt;code&gt;php&lt;/code&gt; image doesn’t run in a container, we can read the Dockerfiles of them.&lt;/p&gt;

&lt;p&gt;And here they are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/_/httpd/"&gt;httpd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/_/php/"&gt;php&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s start examining first the &lt;code&gt;httpd&lt;/code&gt; page.&lt;/p&gt;

&lt;p&gt;If your read it, there is a section called “How to use this image.” that states this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This image only contains Apache httpd with the defaults from upstream. There is no PHP installed, but it should not be hard to extend. On the other hand, if you just want PHP with Apache httpd see the &lt;a href="https://registry.hub.docker.com/_/php/"&gt;PHP image&lt;/a&gt; and look at the &lt;code&gt;-apache&lt;/code&gt; tags.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is clear, now?&lt;/p&gt;

&lt;p&gt;Our service doesn’t interpret PHP files because using the image &lt;code&gt;httpd&lt;/code&gt; simply we don’t have it!&lt;/p&gt;

&lt;p&gt;So, bascially, we are using the wrong image ?.&lt;/p&gt;

&lt;p&gt;Let’s use the right one!&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring an Apache web server with PHP (for real!)
&lt;/h2&gt;

&lt;p&gt;So, we need to use the the &lt;code&gt;php&lt;/code&gt; image: and this is what we were already using.&lt;/p&gt;

&lt;p&gt;But reading the description from the &lt;code&gt;httpd&lt;/code&gt; image, it is not sufficient to use the image &lt;code&gt;php:7.2&lt;/code&gt; like we did: we need to use an image tagged with &lt;code&gt;-apache&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;So, go to &lt;a href="https://hub.docker.com/_/php/"&gt;the &lt;code&gt;php&lt;/code&gt; image page on Docker Hub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you read the page, there is a section called “With Apache” that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;More commonly, you will probably want to run PHP in conjunction with Apache httpd. Conveniently, there’s a version of the PHP container that’s packaged with the Apache web server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the instructions say that we need to use the image &lt;code&gt;php:7.2-apache&lt;/code&gt;: very very well ?&lt;/p&gt;

&lt;p&gt;So, what happened was this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We saw the version of PHP because effectively we were building a container with it, so Docker was able to find it and returned its version;&lt;/li&gt;
&lt;li&gt;We were not able to serve an interpreted PHP file as we were using the Apache container that is not equipped with PHP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, what we need to do now is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change the image we use for PHP in &lt;code&gt;docker-compose.yaml&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Use a &lt;code&gt;Dockerfile&lt;/code&gt; to create the &lt;code&gt;php&lt;/code&gt; service (to merge the current configuration of the Apache container in the PHP container);&lt;/li&gt;
&lt;li&gt;Copy all files from our machine to the new PHP image (instead of copying them in the Apache container);&lt;/li&gt;
&lt;li&gt;Remove the old service with the &lt;code&gt;httpd&lt;/code&gt; image (as it will be substituted by the &lt;code&gt;php:7.2-apache&lt;/code&gt; image);&lt;/li&gt;
&lt;li&gt;Remove the folder &lt;code&gt;docker/build/apache&lt;/code&gt; as not needed anymore.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the resulting &lt;code&gt;docker-compose.yaml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
  # "php" was "language" in previous example
  php:
    build:
      context: .
      dockerfile: docker/build/php/Dockerfile
    ports:
      - "8100:80"

  # Configure the database
  mysql:
    image: mysql:5.7
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root}

  # Configure Apache &amp;lt;- THIS NODE AS TO BE ENTIRELY REMOVED
  # apache:
  # build:
  # context: .
  # dockerfile: docker/build/apache/Dockerfile
  # ports:
  # - "8100:80"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concretely, we have renamed &lt;code&gt;apache&lt;/code&gt; to &lt;code&gt;php&lt;/code&gt; and changed the path to the &lt;code&gt;Dockerfile&lt;/code&gt;: nothing really complex.&lt;/p&gt;

&lt;p&gt;Now the last thing remained is renaming the folder &lt;code&gt;docker/build/apache&lt;/code&gt; in &lt;code&gt;docker/build/php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We also need to change the folder in which we move the files of our app as per &lt;a href="https://hub.docker.com/_/php/"&gt;documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;COPY . /var/www/html/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Where &lt;code&gt;src/&lt;/code&gt; is the directory containing all your PHP code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, in the file &lt;code&gt;docker/build/php/Dockerfile&lt;/code&gt;, we need to change the &lt;code&gt;FROM&lt;/code&gt; instruction to use the image &lt;code&gt;php:7.2-apache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# FROM httpd:2.4 &amp;lt;- Change this to use the PHP image
FROM php:7.2-apache

# Copy Symfony files
# COPY . ./htdocs # &amp;lt;- Remove this
COPY . /var/www/html/ # &amp;lt;- Keep attention: we have removed the "." (dot) at the beginning of the destination path

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last thing to do is building the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:app-aragog-www Aerendir$ docker-compose build
Building php
Step 1/2 : FROM php:7.2-apache
7.2-apache: Pulling from library/php
be8881be8156: Already exists
69a25f7e4930: Already exists
65632e89c5f4: Already exists
cd75fa32da8f: Already exists
15bc7736db11: Pull complete
b2c40cef4807: Pull complete
f3507e55e5eb: Pull complete
e6006cdfa16b: Pull complete
a3ed406e3c88: Pull complete
745f1366071d: Pull complete
bdfcada64ad8: Pull complete
86f2b695cc77: Pull complete
5f634a03970a: Pull complete
a329a7ebde19: Pull complete
fb3d2649f534: Pull complete
Digest: sha256:8188b38abe8f3354862845481452cd3b538bc0648e3c5cdef4ef9ee9365fe2d3
Status: Downloaded newer image for php:7.2-apache
 ---&amp;gt; 5e5a59788e34
Step 2/2 : COPY . /var/www/html/
 ---&amp;gt; a0cfd25e4828

Successfully built a0cfd25e4828
Successfully tagged app-aragog-www_php:latest
mysql uses an image, skipping

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we have removed a service, we need to run first &lt;code&gt;docker-compose down --remove-orphans&lt;/code&gt; and then &lt;code&gt;docker-compose up -d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once containers are created again, we can go to &lt;code&gt;http://127.0.0.1:8100/public&lt;/code&gt; and we will see the most beautiful page in the world ?:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-php-apache-symfony-welcome.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--70JCFKEp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-php-apache-symfony-welcome.png" alt="" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are now ready to start building our Symfony based app… or maybe we aren’t?&lt;/p&gt;

&lt;p&gt;Mmm, no, we are not still ready ???&lt;/p&gt;

&lt;p&gt;What do we need to do now? ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and next steps
&lt;/h2&gt;

&lt;p&gt;Well, also if we are able to copy the Symfony’s files in the web server container (that now is not &lt;code&gt;httpd&lt;/code&gt; anymore, but, instead, &lt;code&gt;php:7.2-apache&lt;/code&gt;) and we are able to make the web server serve interpreted PHP files, we still have some other things to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand why we can see Symfony only pointing directly to the &lt;code&gt;public&lt;/code&gt; folder (SPOILER: &lt;code&gt;DocumentRoot&lt;/code&gt;?);&lt;/li&gt;
&lt;li&gt;Checking our web server meets all the Symfony’s requirements;&lt;/li&gt;
&lt;li&gt;Maybe, also refine our copying of files as it is currently very dirty and heavy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for the moment we have reached some great goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We know how to create a real Apache web server on Docker;&lt;/li&gt;
&lt;li&gt;We know how to deal with images, to understand how to chose and use them (now, for example, you can use MongoDB if you like: go to search for it!);&lt;/li&gt;
&lt;li&gt;We know how to copy files in the containers and in doing this, we have understood what are &lt;code&gt;Dockerfile&lt;/code&gt;s (one of the foundation of Docker!), what is the Docker Context, how it works and how we can manipulate it (almost: there is at least one other thing you need to know to master it… Next post!).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are a lot of things and we are closer to our goal of fully using Symfony on Docker!&lt;/p&gt;

&lt;p&gt;The next post will teach you how to solve the problems I mentioned.&lt;/p&gt;

&lt;p&gt;In the meantime, remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/moving-symfony-files-in-the-docker-container/"&gt;Moving Symfony files in the Docker container&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>docker</category>
      <category>php</category>
      <category>symfony</category>
    </item>
    <item>
      <title>How to dockerize a Symfony application: Introduction and the web server</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Wed, 21 Nov 2018 12:54:29 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-dockerize-a-symfony-application-introduction-and-the-web-server-adp</link>
      <guid>https://dev.to/serendipityhq/how-to-dockerize-a-symfony-application-introduction-and-the-web-server-adp</guid>
      <description>&lt;p&gt;Starting to use Docker is not an easy task. Starting to use Symfony on Docker is far more complex.&lt;/p&gt;

&lt;p&gt;There are too many things to understand before being able to dockerize a Symfony application.&lt;/p&gt;

&lt;p&gt;I studied Docker for more than one month before being able to dockerize our Symfony applications.&lt;/p&gt;

&lt;p&gt;How did I reach the goal? By trial and error. A lot of trial and error. And a lot of reading. Really really a lot of reading.&lt;/p&gt;

&lt;p&gt;But I can make your journey a lot easier and this series of posts is exactly this: the final summary of my journey trying to dockerize our Symfony applications here at Serendipity HQ.&lt;/p&gt;

&lt;p&gt;Unless you have lived on Mars in the last months, you have heard of Docker for sure.&lt;/p&gt;

&lt;p&gt;And if you are a curious person, maybe the idea of trying it came to your mind.&lt;/p&gt;

&lt;p&gt;But starting to use Docker is not an easy task, at least it wasn’t easy for me.&lt;/p&gt;

&lt;p&gt;I read a lot of articles about how to set up a Symfony app to use Docker and all that I found was a bunch of short articles with some code to copy and paste without any explanation about what each line did and why it was there.&lt;/p&gt;

&lt;p&gt;So I switched to analyse other Symfony projects that already use Docker (you will find a list of them at the end of this post), but the problem was the opposite: very complex configurations, impossible to understand nor to modify without an understanding of what each line did.&lt;/p&gt;

&lt;p&gt;And also searching for each instruction didn’t help as what I lacked was the general picture.&lt;/p&gt;

&lt;p&gt;So I decided to start from scratch, solving a problem after problem each time one arose.&lt;/p&gt;

&lt;p&gt;And this series of posts is the result of this journey: configuring a Symfony app to use Docker.&lt;/p&gt;

&lt;p&gt;Before starting to dockerize your Symfony application, it is better to explain what precisely Docker is.&lt;/p&gt;

&lt;p&gt;Docker is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;… An open platform for developers and sysadmins to build, ship, and run distributed applications, whether on laptops, data center VMs, or the cloud.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its main concept is &lt;a href="https://www.docker.com/get-started"&gt;the “container”&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Docker containers wrap up software and its dependencies into a standardized unit for software development that includes everything it needs to run: code, runtime, system tools and libraries. This guarantees that your application will always run the same and makes collaboration as simple as sharing a container image.&lt;/p&gt;

&lt;cite&gt;&lt;a href="https://www.docker.com/get-started"&gt;Docker for Developers &amp;gt; Get started&lt;/a&gt;&lt;/cite&gt;
&lt;/blockquote&gt;

&lt;p&gt;The main advantages of using Docker are:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Onboard faster and stop wasting hours trying to set up development environments, spin up new instances and make copies of production code to run locally.&lt;/li&gt;
&lt;li&gt;Enable polyglot development and use any language, stack or tools without worry of application conflicts.&lt;/li&gt;
&lt;li&gt;Eliminate environment inconsistencies and the “works on my machine” problem by packaging the application, configs and dependencies into an isolated container.&lt;/li&gt;
&lt;li&gt;Alleviate concern over application &lt;a href="https://www.docker.com/products/security"&gt;security&lt;/a&gt;
&lt;cite&gt;&lt;a href="https://www.docker.com/get-started"&gt;Docker for Developers &amp;gt; Get started&lt;/a&gt;&lt;/cite&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, now that we have a clear picture of what is Docker and which problems it solves, let’s start to go a bit deeper in the topic.&lt;/p&gt;

&lt;p&gt;The first thing I needed to learn was Docker Compose, the builder of our entire stack.&lt;/p&gt;

&lt;p&gt;Our stack uses a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file put in the root of the application.&lt;/p&gt;

&lt;p&gt;But now I’m going too fast: Let’s try to do one step at time.&lt;/p&gt;

&lt;p&gt;Doing this, at the end of this series, you will have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clear picture of how each building block fits into the bigger picture;&lt;/li&gt;
&lt;li&gt;Which are those building blocks;&lt;/li&gt;
&lt;li&gt;The tools you need to debug Docker and better understand what is going on;&lt;/li&gt;
&lt;li&gt;A basic configuration from which to start building your next dockerized application;&lt;/li&gt;
&lt;li&gt;The tools required to expand this basic configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this series of posts, we are going to build a Dockerized application based on Symfony 4.2.&lt;/p&gt;

&lt;p&gt;But keep in mind that the Symfony version is really irrelevant: once you have understood the basics you will be able to dockerize any version of Symfony, present, past and future.&lt;/p&gt;

&lt;p&gt;So, please, before starting, &lt;a href="https://symfony.com/doc/current/setup.html"&gt;create a new Symfony project to dockerize&lt;/a&gt;: having a test application makes things easier as it permits you to focus on the basics, without having to deal with your custom code or custom configurations, external services, etc.&lt;/p&gt;

&lt;p&gt;As already told, once you will have the basics, you can dockerize any Symfony project!&lt;/p&gt;

&lt;p&gt;Then, install Docker searching on Google “&lt;a href="https://www.google.it/search?q=how+to+install+Docker"&gt;How to install Docker&lt;/a&gt;“: Google is really better than me at identifying your OS and giving you the right steps to install Docker, so, follow him!?&lt;/p&gt;

&lt;p&gt;We are ready!&lt;/p&gt;

&lt;p&gt;Just one note before really starting: in this series of post I will make you follow the exact trial-and-arr approach I followed the first time.&lt;/p&gt;

&lt;p&gt;This way you will really understand which are the problems you will face when dockerizing any app and you will deeply understand the logic behind a dockerized infrastructure.&lt;/p&gt;

&lt;p&gt;When someone starts using Docker not all concepts are obvious and the approach someone may use or the logic (s)he will follow or the assumptions (s)he will have may be wrong.&lt;/p&gt;

&lt;p&gt;For this reason, I prefer to make you able to make errors: so, in this series of posts we will make errors (very bad?) but we will also fix them learning really how Docker works and ” thinks”!?&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing &lt;code&gt;docker-compose&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Ok, now that you have a fresh Symfony app to dockerize and have installed Docker on your computer, let’s start understanding the first building block of our Docker infrastructure: Docker Compose.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Compose is a tool for defining and running multi-container Docker applications.&lt;/p&gt;

&lt;p&gt;With Compose, you use a YAML file to configure your application’s services.&lt;/p&gt;

&lt;p&gt;Then, with a single command, you create and start all the services from your configuration.&lt;/p&gt;

&lt;p&gt;To learn more about all the features of Compose, see &lt;a href="https://docs.docker.com/compose/overview/#features"&gt;the list of features&lt;/a&gt;.&lt;/p&gt;

&lt;cite&gt;&lt;a href="https://docs.docker.com/compose/overview/"&gt;Overview of Docker Compose&lt;/a&gt;&lt;/cite&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ok, I know, this can be a bit obscure at the beginning: after all, you may have no idea of what a container is concretely, what it does and of which parts is composed of.&lt;/p&gt;

&lt;p&gt;Don’t worry: keep reading and you will understand everything!&lt;/p&gt;

&lt;p&gt;For the moment, you only need to understand and remember that Docker Compose uses a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file put in the root folder of the project to know what services it has to create to make your app able to work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Compose file is a YAML file defining services, networks, and volumes for a Docker application.&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;There are several versions of the Compose file format – 1, 2, 2.x, and 3.x&lt;/p&gt;

&lt;cite&gt;&lt;a href="https://docs.docker.com/compose/compose-file/compose-versioning/#compatibility-matrix"&gt;Compose file versions and upgrading&lt;/a&gt;&lt;/cite&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mmm, services, networks, volumes, file versions: these are exactly the kind of things that make difficult to understand how to use Docker at the beginning. ?&lt;/p&gt;

&lt;p&gt;Again, don’t worry and keep reading: I will explain to you everything in details. More often than not, it is easier to do things first and understand them later, instead of trying to understand them first and then doing them later.&lt;/p&gt;

&lt;p&gt;So, how you write your Compose file, depends on the version of Docker you are running.&lt;/p&gt;

&lt;p&gt;To know which version of Docker you are running, use the &lt;code&gt;docker version&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker version
Client:
 Version: 18.06.0-ce
 API version: 1.38
 Go version: go1.10.3
 Git commit: 0ffa825
 Built: Wed Jul 18 19:05:26 2018
 OS/Arch: darwin/amd64
 Experimental: false

Server:
 Engine:
  Version: 18.06.0-ce
  API version: 1.38 (minimum version 1.12)
  Go version: go1.10.3
  Git commit: 0ffa825
  Built: Wed Jul 18 19:13:46 2018
  OS/Arch: linux/amd64
  Experimental: false

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, I’m now running Docker version &lt;code&gt;18.06.0-ce&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means that I can use the Compose file format version 3.7.&lt;/p&gt;

&lt;p&gt;How do I know this?&lt;/p&gt;

&lt;p&gt;Because Docker provides a &lt;a href="https://docs.docker.com/compose/compose-file/compose-versioning/#compatibility-matrix"&gt;matrix &lt;/a&gt;where, for each Docker version, there is the corresponding Compose version supported.&lt;/p&gt;

&lt;p&gt;Very easy, isn’t it? ?&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating our &lt;code&gt;docker-compose.yaml&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;For a Symfony application we basically need three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A web server (Apache or Nginx);&lt;/li&gt;
&lt;li&gt;PHP;&lt;/li&gt;
&lt;li&gt;A database (MySQL, PostgreSQL, MongoDB, Redis, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last but not least, we also need an operating system: MacOS, Windows, *nix, etc.&lt;/p&gt;

&lt;p&gt;The combination of those technologies is our “stack”.&lt;/p&gt;

&lt;p&gt;The stack we will build will have these characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It has to be easily configurable;&lt;/li&gt;
&lt;li&gt;It has to have the very basic requirements needed to run Symfony;&lt;/li&gt;
&lt;li&gt;It has a high probability, so we can use it in other projects without Docker to start dockerize them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, our stack will be this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operating system: Ubuntu;&lt;/li&gt;
&lt;li&gt;PHP Version: 7.2 (this is the best choice, simply);&lt;/li&gt;
&lt;li&gt;Database: MySQL;&lt;/li&gt;
&lt;li&gt;Web server: Apache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m going to use Apache because if you develop PHP applications, it is 99% probable that you are familiar with it.&lt;/p&gt;

&lt;p&gt;Nginx is less common and requires another series of posts to be understood?.&lt;/p&gt;

&lt;p&gt;Let’s maintain things simple: use Apache!&lt;/p&gt;

&lt;p&gt;The same for MySQL: for more complex applications you may need MongoDB, but for a basic configuration and, mostly, to understand the basics of Docker, it is better to use something simple and familiar as MySQL instead of MongoDB of which you may have never heard of (do you live on Mars??).&lt;/p&gt;

&lt;p&gt;To get this stack up and running we have to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file in the root folder of our project;&lt;/li&gt;
&lt;li&gt;Define the components of our stack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, let’s start with PHP creating a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file in the root folder of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# docker-compose.yaml

version: '3.7'
services:
  language:
    image: php:7.2

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can run &lt;code&gt;docker-compose up -d&lt;/code&gt; and we will have a service with PHP up and running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Creating network "app-test-www_default" with the default driver
Creating app-test-www_language_1 ... done
Attaching to app-test-www_language_1
language_1 | Interactive shell
language_1 | 
app-test-www_language_1 exited with code 0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker will start downloading a lot of things and the process may take a bit of time: be patient!?&lt;/p&gt;

&lt;p&gt;When the exit code is &lt;code&gt;0&lt;/code&gt;, then all gone well.&lt;/p&gt;

&lt;p&gt;NOTE: the &lt;code&gt;-d&lt;/code&gt; flag means “detached”: this makes the console available again after Docker Compose finishes. If you don’t pass it, you will need to open a new console window to continue to use the command line.&lt;/p&gt;

&lt;p&gt;Things to note here&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;version&lt;/code&gt;: is the version of the compose file.
As told above, the version you use in your compose file depends on the Docker version you are running. Check the &lt;a href="https://docs.docker.com/compose/compose-file/compose-versioning/#compatibility-matrix"&gt;compatibility matrix&lt;/a&gt; for more information.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;services.language&lt;/code&gt;: “language” here is the name of the service we are creating. It is an arbitrary string we use to name the service.
Change this to “php” if you like: I used “language” just to show you how does it work and that it can be changed and is not related in any way to the image you are using.
You can call the service as you like, also &lt;code&gt;my_astonishing_and_super_power_service&lt;/code&gt; (but I warmly suggest you to use really shorter names!?)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service.language.image&lt;/code&gt;: defines the image we want to use to build the service.
Here we are using the image of PHP 7.2. Later in this series of posts, I will explain to you how to find the images you may need and how to understand how to use them: be patient!?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, we now have PHP configured: we still need other components to make our Symfony app run on Docker.&lt;/p&gt;

&lt;p&gt;We need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The operating system;&lt;/li&gt;
&lt;li&gt;The web server;&lt;/li&gt;
&lt;li&gt;A database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s configure them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
  # "php" was "language" in previous example
  php:
    image: php:7.2

  # Configure the database
  mysql:
    image: mysql:5.7

  # Configure Apache
  apache:
    image: httpd:2.4

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, running again &lt;code&gt;docker-compose up -d&lt;/code&gt; reports this (note that Docker may first download images, so you will see a lot of logging before the lines below):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker-compose up -d
Starting app-test-www_mysql_1 ... done
Starting app-test-www_php_1 ... done
Creating app-test-www_apache_1 ... done

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we have all the services up and running.&lt;/p&gt;

&lt;p&gt;To verify this, other than the previous messages with “done”, you can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6d9e627c81f5 httpd:2.4 "httpd-foreground" 18 minutes ago Up 18 minutes 80/tcp app-test-www_apache_1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, currently I have only one container running and it is the Apache one.&lt;/p&gt;

&lt;p&gt;If you are asking yourself, yes, I run this command 18 minutes later because in the meantime I gone out with Loki, my pet. &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mBsDkgDd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/2665.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mBsDkgDd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/2665.png" alt="♥" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ED9p03RV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/loki-min.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ED9p03RV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/loki-min.jpeg" alt="" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Ok, he’s not a cat, but he is anyway a really pretty dog!?)&lt;/p&gt;

&lt;p&gt;Another thing you should note: there is only one service, the Apache one: where are the others? There seems that MySQL and PHP aren’t running ?&lt;/p&gt;

&lt;p&gt;But anyway there is a container running: Let’s go exploring it without taking care of the others (for the moment!?).&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring the Apache container
&lt;/h2&gt;

&lt;p&gt;To explore the container running Apache, use the command &lt;a href="https://stackoverflow.com/a/48428930/1399706"&gt;&lt;code&gt;docker exec -it [container_id] bash&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker exec -it 6d9e627c81f5 bash
root@6d9e627c81f5:/usr/local/apache2# ls
bin build cgi-bin conf error htdocs icons include logs modules

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NOTE: the &lt;code&gt;-i&lt;/code&gt; flag stands for “–interactive” and makes possible to interact with the console that we called with the &lt;code&gt;-t&lt;/code&gt; flag, that allocates a pseudo-tty console.&lt;br&gt;&lt;br&gt;
So, instead of using &lt;code&gt;docker exec -i -t ...&lt;/code&gt;, we condensed the command into &lt;code&gt;docker exec -it ...&lt;/code&gt; . More info &lt;a href="https://docs.docker.com/engine/reference/run/#foreground"&gt;here&lt;/a&gt; and &lt;a href="https://stackoverflow.com/a/32231458/1399706"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As you can see, we have some folders: from here you can go around with the commands &lt;code&gt;cd&lt;/code&gt; to change the directory (ex.: &lt;code&gt;cd bin&lt;/code&gt;) and with the command &lt;code&gt;ls&lt;/code&gt; to see what’s inside the current folder (ex.: &lt;code&gt;ls&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@6d9e627c81f5:/usr/local/apache2# cd bin 
root@6d9e627c81f5:/usr/local/apache2/bin# ls
ab apachectl apxs checkgid dbmmanage envvars envvars-std fcgistarter htcacheclean htdbm htdigest htpasswd httpd httxt2dbm logresolve rotatelogs suexe

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To come back to your user, out of Docker, simply type &lt;code&gt;exit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@6d9e627c81f5:/usr/local/apache2/bin# exit
exit
MacBook-Pro-di-Aerendir:app-test-www Aerendir$

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we know how to enter a container, it’s time to understand where are our MySQL and PHP: are they lost?&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging MySQL and PHP in Docker
&lt;/h2&gt;

&lt;p&gt;We defined three services in our &lt;code&gt;docker-compose.yaml&lt;/code&gt; file: Apache, MySQL and PHP.&lt;/p&gt;

&lt;p&gt;But our container seems have lost MySQL and PHP: where are they gone?&lt;/p&gt;

&lt;p&gt;If you go to some paragraphs above, you notice we used the command &lt;code&gt;docker-compose up -d&lt;/code&gt;: the &lt;code&gt;-d&lt;/code&gt;flag means “detached”, so we have again the console available.&lt;/p&gt;

&lt;p&gt;Try to not use the &lt;code&gt;-d&lt;/code&gt; flag to run the command (first, run &lt;code&gt;docker-compose down&lt;/code&gt;!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker-compose down
Stopping app-test-www_apache_1 ... done
Removing app-test-www_mysql_1 ... done
Removing app-test-www_php_1 ... done
Removing app-test-www_apache_1 ... done
Removing network app-test-www_default

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we can &lt;code&gt;up&lt;/code&gt; again to see what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker-compose up
Creating network "app-test-www_default" with the default driver
Creating app-test-www_mysql_1 ... done
Creating app-test-www_apache_1 ... done
Creating app-test-www_php_1 ... done
Attaching to app-test-www_mysql_1, app-test-www_apache_1, app-test-www_php_1
mysql_1 | error: database is uninitialized and password option is not specified 
mysql_1 | You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD
php_1 | Interactive shell
php_1 | 
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | [Sun Aug 12 15:14:59.981146 2018] [mpm_event:notice] [pid 1:tid 140196935767936] AH00489: Apache/2.4.33 (Unix) configured -- resuming normal operations
apache_1 | [Sun Aug 12 15:14:59.981380 2018] [core:notice] [pid 1:tid 140196935767936] AH00094: Command line: 'httpd -D FOREGROUND'
app-test-www_mysql_1 exited with code 1
app-test-www_php_1 exited with code 0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, using the detached mode all seems going well; but removing the &lt;code&gt;-d&lt;/code&gt; flag, some errors seem to arise:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;mysql_1 | error: database is uninitialized and password option is not specified&lt;br&gt;&lt;br&gt;
apache_1 | AH00558: httpd: Could not reliably determine the server’s fully qualified domain name, using 192.168.96.3. Set the ‘ServerName’ directive globally to suppress this message&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So we have two errors: one from Apache and one from MySQL: Let’s fix them!&lt;/p&gt;

&lt;h3&gt;
  
  
  Further configuring MySQL
&lt;/h3&gt;

&lt;p&gt;MySQL returned this error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;mysql1 | error: database is uninitialized and password option is not specified&lt;br&gt;&lt;br&gt;
mysql_1 | You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, it is obvious what we have to do to fix it: set the required environment variables!&lt;/p&gt;

&lt;p&gt;So, add the node &lt;code&gt;services.mysql.environment&lt;/code&gt; and set the variable &lt;code&gt;MYSQL_ROOT_PASSWORD&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
    # "php" was "language" in previous example
    ...

    # Configure the database
    mysql:
        image: mysql:5.7
        # Add the MYSQL_ROOT_PASSWORD to the environment variables
        environment:
          - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root}

    # Configure Apache
    ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NOTE: We are using a default value of &lt;code&gt;root&lt;/code&gt; if the env variable &lt;code&gt;MYSQL_ROOT_PASSWORD&lt;/code&gt; is not set; if it is set, instead, we use its value. This syntax is of Bash and is the one to set &lt;a href="http://wiki.bash-hackers.org/syntax/pe#use_a_default_value"&gt;default values&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is useful as it permits us to use an &lt;code&gt;.env&lt;/code&gt; file locally (read by the &lt;a href="https://symfony.com/doc/current/components/dotenv.html"&gt;Symdony’s Dotenv Component&lt;/a&gt;) while using real environment variables on production.&lt;/p&gt;

&lt;p&gt;Now, on your keyboard, press &lt;code&gt;CTRL + C&lt;/code&gt; to kill the current Docker container, then run &lt;code&gt;docker-compose up&lt;/code&gt; (without &lt;code&gt;-d&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker-compose up
Recreating app-test-www_mysql_1 ... done
Starting app-test-www_php_1 ... done
Starting app-test-www_apache_1 ... done
Attaching to app-test-www_php_1, app-test-www_apache_1, app-test-www_mysql_1
php_1 | Interactive shell
php_1 | 
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
apache_1 | [Sun Aug 12 15:21:41.877927 2018] [mpm_event:notice] [pid 1:tid 139714071852928] AH00489: Apache/2.4.33 (Unix) configured -- resuming normal operations
apache_1 | [Sun Aug 12 15:21:41.878086 2018] [core:notice] [pid 1:tid 139714071852928] AH00094: Command line: 'httpd -D FOREGROUND'
mysql_1 | Initializing database
...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console will continue to give you log messages that tell you what is happening for MySQL service.&lt;/p&gt;

&lt;p&gt;It seems that the MySQL errors are gone: let’s verify the container started.&lt;/p&gt;

&lt;p&gt;You are not in detached mode, so, open a new console window and use the &lt;code&gt;docker ps&lt;/code&gt; command to see the currently active containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$: docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
baa0652ed2a6 mysql:5.7 "docker-entrypoint.s…" 4 minutes ago Up 4 minutes 3306/tcp app-test-www_mysql_1
f1da511ff5b5 httpd:2.4 "httpd-foreground" 4 minutes ago Up 4 minutes 80/tcp app-test-www_apache_1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we now have two services running in our container: we have fixed MySQL!?&lt;/p&gt;

&lt;p&gt;The last one remaining is PHP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further configuring PHP
&lt;/h3&gt;

&lt;p&gt;Where is PHP? Our container only shows Apache and MySQL and there is no trace of PHP.&lt;/p&gt;

&lt;p&gt;Let’s start from the beginning…&lt;/p&gt;

&lt;p&gt;On any machine you run &lt;code&gt;php&lt;/code&gt;, to see if it is running, you can simply use the command &lt;code&gt;php -v&lt;/code&gt;: this will return the version of PHP currently used (the below command is run on my machine, not on any Docker container!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ php -v
PHP 7.2.2 (cli) (built: Feb 1 2018 13:23:34) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies
    with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans
    with blackfire v1.21.0~mac-x64-non_zts72, https://blackfire.io, by Blackfire

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, on my machine I’m using version 7.2 of PHP and I also have OPcache, Xdebug and Blackfire.&lt;/p&gt;

&lt;p&gt;But how to run this command in a Docker container?&lt;/p&gt;

&lt;p&gt;Try this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker run php -v
PHP 7.2.8 (cli) (built: Jul 21 2018 07:47:51) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Olè! PHP is running!&lt;/p&gt;

&lt;p&gt;The version is the 7.2.8, different than the version 7.2.2 that is run on my local machine, and there is no trace of OPcache nor of Xdebug or of Blackfire.&lt;/p&gt;

&lt;p&gt;So, we have basically nothing to debug: we have all the three services up and running!&lt;/p&gt;

&lt;p&gt;Yes, I know what you are asking: then, if PHP is running, why isn’t it shown in the list of running containers?&lt;/p&gt;

&lt;p&gt;I will clarify this later in this series of posts, for the moment put apart this question.&lt;/p&gt;

&lt;p&gt;SPOILER: We made an error configuring the PHP service!?…?&lt;/p&gt;

&lt;p&gt;Now it is time to access the web server from the browser!&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessing the web server from the browser
&lt;/h2&gt;

&lt;p&gt;Before accessing our Symfony app from the browser, we have to first figure out how to simply access the web server from the browser.&lt;/p&gt;

&lt;p&gt;Let’s run again the &lt;code&gt;ps&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
baa0652ed2a6 mysql:5.7 "docker-entrypoint.s…" 19 minutes ago Up 19 minutes 3306/tcp app-test-www_mysql_1
f1da511ff5b5 httpd:2.4 "httpd-foreground" 19 minutes ago Up 19 minutes 80/tcp app-test-www_apache_1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the &lt;code&gt;httpd:2.4&lt;/code&gt; image is exposing the port &lt;code&gt;80&lt;/code&gt;: this is the default port used by the Apache web server for TCP connections.&lt;/p&gt;

&lt;p&gt;But this port is exposed on Docker, not on our computer.&lt;/p&gt;

&lt;p&gt;In fact, if you try to access &lt;code&gt;http://localhost:80&lt;/code&gt; you will continue to see your local web server.&lt;/p&gt;

&lt;p&gt;How can we access the container’s web server instead?&lt;/p&gt;

&lt;p&gt;We need to bind the port &lt;code&gt;80&lt;/code&gt; from the Docker container to one of the ports of our computer.&lt;/p&gt;

&lt;p&gt;The Docker’s documentation about &lt;a href="https://docs.docker.com/config/containers/container-networking/#published-ports"&gt;Container networking&lt;/a&gt; states this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By default, when you create a container, it does not publish any of its ports to the outside world.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then it suggests using the flag &lt;code&gt;--publish&lt;/code&gt;, but this is not useful in our case: we need to understand how to bind Docker ports to our machine ports (our machine is the Docker’s host), but using the Compose file.&lt;/p&gt;

&lt;p&gt;This means a simple thing: you cannot access the services from your browser as Docker doesn’t make them “public”: they exist, but can be accessed only by other Docker’s services.&lt;/p&gt;

&lt;p&gt;If you want to access the services from the world outside Docker, then you need to “bind” them.&lt;/p&gt;

&lt;p&gt;The “binding” is a way of saying to Docker that all the traffic to the port of your computer has to be forwarded to the port it internally assigns to the service.&lt;/p&gt;

&lt;p&gt;So, anticipating what we will do, we will bind the port &lt;code&gt;80&lt;/code&gt; of the Docker service to the port &lt;code&gt;8100&lt;/code&gt; of our computer. This will tell Docker that all the traffic to the port &lt;code&gt;8100&lt;/code&gt; on our computer has to be forwarded to the port &lt;code&gt;80&lt;/code&gt; he has mapped internally.&lt;/p&gt;

&lt;p&gt;Docker offers two ways to create the “binding”.&lt;/p&gt;

&lt;p&gt;So, let’s read the documentation of Compose file: there we can find two useful keys: &lt;a href="https://docs.docker.com/compose/compose-file/#expose"&gt;&lt;code&gt;expose&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.docker.com/compose/compose-file/#ports"&gt;&lt;code&gt;ports&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Expose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expose ports without publishing them to the host machine – they’ll only be accessible to linked services. Only the internal port can be specified.&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ports&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expose ports.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not so useful as descriptions, aren’t they? Honestly, they are also a bit confusing ?&lt;/p&gt;

&lt;p&gt;Let’s try to clarify their meaning.&lt;/p&gt;

&lt;p&gt;Basically, both &lt;code&gt;expose&lt;/code&gt; and &lt;code&gt;ports&lt;/code&gt; makes possible for a Docker container to make some ports reachable.&lt;/p&gt;

&lt;p&gt;The main differences between the two are these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;expose&lt;/code&gt; makes those ports reachable only by services/containers in the same Docker network, but not from outside of it;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ports&lt;/code&gt;, instead, makes the ports reachable from outside the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, guess what, our best option is using &lt;code&gt;ports&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Docker’s documentation about ports clearly states a caveat:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: When mapping ports in the &lt;code&gt;HOST:CONTAINER&lt;/code&gt; format, you may experience erroneous results when using a container port lower than 60, because YAML parses numbers in the format &lt;code&gt;xx:yy&lt;/code&gt; as a base-60 value. For this reason, we recommend always explicitly specifying your port mappings as strings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is relevant because you can do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
  apache:
    ...
    ports:
      - 80

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or you can do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
  apache:
    ...
    ports:
      - '8100:80'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first case, we can simply use an integer, but in the second case, as we are using the format &lt;code&gt;HOST:CONTAINER&lt;/code&gt;, we need to set the port mapping as strings (using “‘” – an “apex”) and not as integers to avoid the before mentioned problems in parsing.&lt;/p&gt;

&lt;p&gt;More, in the first case, the port &lt;code&gt;80&lt;/code&gt; will be bound to a random port of the host machine (again, the host machine is our machine that is running Docker): this is not useful, as with a random port we cannot anyway access the Apache server running in the container! (each time we should first check which is the current port assigned – using &lt;code&gt;docker ps&lt;/code&gt; – and this is not useful).&lt;/p&gt;

&lt;p&gt;So, our port binding of Apache ports is something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.7'
services:
  # "php" was "language" in previous example
  php:
    ...

  # Configure the database
  mysql:
    ...

  # Configure Apache
  apache:
    image: httpd:2.4
    ports:
      - "8100:80"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the command &lt;code&gt;docker-compose up -d&lt;/code&gt; to apply the new configuration.&lt;/p&gt;

&lt;p&gt;Now, accessing the URL &lt;code&gt;http://127.0.0.1:8100&lt;/code&gt; we will see the default Apache &lt;code&gt;index.html&lt;/code&gt; page with the famous “It Works!” message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-port-binding.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ke283tum--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/08/docker-apache-port-binding.png" alt="" width="425" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But where is this file located?&lt;/p&gt;

&lt;p&gt;Good, a bit of context.&lt;/p&gt;

&lt;p&gt;Apache stores the files to be served in the so-called “&lt;a href="https://httpd.apache.org/docs/2.4/urlmapping.html#documentroot"&gt;document root&lt;/a&gt;“.&lt;/p&gt;

&lt;p&gt;So we need to find this document root in our container.&lt;/p&gt;

&lt;p&gt;As already shown above, run &lt;code&gt;docker ps&lt;/code&gt; to get the id of the container we want to enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
718b8afc0626 mysql:5.7 "docker-entrypoint.s…" 34 minutes ago Up 5 seconds 3306/tcp app-test-www_mysql_1
57aa966336a2 httpd:2.4 "httpd-foreground" 34 minutes ago Up 5 seconds 8100-&amp;gt;80/tcp app-test-www_apache_1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we enter the container running this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Aerendir$ docker exec -it 57aa966336a2 bash

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can start exploring the contents of the container with &lt;code&gt;ls&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@57aa966336a2:/usr/local/apache2# ls
bin build cgi-bin conf error htdocs icons include logs modules

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hey, &lt;code&gt;htdocs&lt;/code&gt; is one of the default names of the Apache’s document root folders!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@57aa966336a2:/usr/local/apache2# cd htdocs
root@57aa966336a2:/usr/local/apache2/htdocs# ls
index.html

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Found! The file &lt;code&gt;index.html&lt;/code&gt; is the one that showed us the message “It works!”: this is our document root!&lt;/p&gt;

&lt;p&gt;We spotted it!?&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and next steps
&lt;/h2&gt;

&lt;p&gt;For this post I think we have covered a lot of things:&lt;/p&gt;

&lt;p&gt;We learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to configure services with Docker;&lt;/li&gt;
&lt;li&gt;How to access them;&lt;/li&gt;
&lt;li&gt;Some basic debugging techniques;&lt;/li&gt;
&lt;li&gt;How to bind the ports of a Docker’s services to the ports of our computer;&lt;/li&gt;
&lt;li&gt;How to access the web server from our browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In doing this we also learned some basic concepts of Docker like what is a container, what is a service, some basic information about the networking capabilities of Docker (the binding of ports is “networking”!).&lt;/p&gt;

&lt;p&gt;But now we also have some questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the PHP service doesn’t appear in the list of services?&lt;/li&gt;
&lt;li&gt;And why, also if it doesn’t appear in such list, anyway PHP seems to be running?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And most important: now that we have our web server up and running, how can we move our Symfony files into it to start developing our app using Docker?&lt;/p&gt;

&lt;p&gt;Duh, a lot of questions! (and many more will arise!?).&lt;/p&gt;

&lt;p&gt;Well, trust me, I will answer all of them in the next post of this series.&lt;/p&gt;

&lt;p&gt;For the moment, I will answer the last question: how to move the Symfony’s files into the web server run by Docker.&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;In the meantime, I wish you flocking users!&lt;/p&gt;

&lt;p&gt;Next post: &lt;a href="https://io.serendipityhq.com/experience/moving-symfony-files-in-the-docker-container/"&gt;Moving Symfony files in the Docker container&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/symfony-on-docker/"&gt;How to dockerize a Symfony application: Introduction and the web server&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>docker</category>
      <category>php</category>
      <category>symfony</category>
    </item>
    <item>
      <title>How to set up the workflow to develop a Genestrap-based WordPress theme</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Mon, 19 Nov 2018 18:32:28 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-set-up-the-workflow-to-develop-a-genestrap-based-wordpress-theme-2ach</link>
      <guid>https://dev.to/serendipityhq/how-to-set-up-the-workflow-to-develop-a-genestrap-based-wordpress-theme-2ach</guid>
      <description>&lt;p&gt;The main advantage of having a Genestrap-based WordPress theme is the ability to use the tools and the techniques you already use to develop your web apps.&lt;/p&gt;

&lt;p&gt;Using a framework like Genesis to develop a WordPress theme takes the management of WordPress to a whole new level of complexity (and fun, too!).&lt;/p&gt;

&lt;p&gt;So, once you have created the &lt;a href="https://dev.to/aerendir/how-to-create-your-custom-wordpress-theme-with-genestrap-45mj-temp-slug-8356768"&gt;first version of your Genestrap-based WordPress theme&lt;/a&gt;, let’s see how to prepare your environment to manage the updates you will do developing your custom theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up &lt;code&gt;git&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you open a console window and write &lt;code&gt;git remote&lt;/code&gt;: you will see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote
origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you can write this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote show origin
* remote origin
Fetch URL: https://github.com/Aerendir/Genestrap.git
Push URL: https://github.com/Aerendir/Genestrap.git
HEAD branch: master
Remote branch:
master tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (local out of date)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the current repository is set to point to the main Genestrap repository on GitHub.&lt;/p&gt;

&lt;p&gt;So, what we need to do, is to point it to one of our custom repositories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a private &lt;code&gt;git&lt;/code&gt; repository
&lt;/h3&gt;

&lt;p&gt;The first thing you need to do is to create a private &lt;code&gt;git&lt;/code&gt; repository in which store your developing Genestrap-based WordPress theme.&lt;/p&gt;

&lt;p&gt;I use BitBucket that offers free private repository (with some limitations on the number of users, but this is not a problem for a project like this).&lt;/p&gt;

&lt;p&gt;Obviously, you can use what you like: BitBucket, GitHub, GitLab or anything else.&lt;/p&gt;

&lt;p&gt;I called my repo &lt;code&gt;web-serendipityhq-wp_theme&lt;/code&gt; and below you can see my empty repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yr0zODmf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/bitbucket-genestrap-theme-empty-repo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yr0zODmf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/bitbucket-genestrap-theme-empty-repo.png" alt="" width="800" height="676"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we have a new empty repository to fill, let’s fill it!&lt;/p&gt;

&lt;h3&gt;
  
  
  Pushing your Genestrap-based WordPress theme to your private &lt;code&gt;git&lt;/code&gt; repository
&lt;/h3&gt;

&lt;p&gt;To push your Genestrap-based WordPress theme to your just created &lt;code&gt;git&lt;/code&gt; repository, you need to change the &lt;code&gt;origin&lt;/code&gt; remote from the current pointing to Genestrap’s GitHub repository to a new one pointing to your just created repo.&lt;/p&gt;

&lt;p&gt;Doing this is really simple with a command like &lt;code&gt;git remote remove&lt;/code&gt; and &lt;code&gt;git remote add&lt;/code&gt; (run &lt;code&gt;git remote --help&lt;/code&gt; for the full list of options).&lt;/p&gt;

&lt;p&gt;So, start by removing the current &lt;code&gt;remote&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote rm origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NOTE: &lt;code&gt;rm&lt;/code&gt; is the short version of &lt;code&gt;remove&lt;/code&gt; and you can use both as you like.&lt;/p&gt;

&lt;p&gt;Now, add a new &lt;code&gt;remote&lt;/code&gt; pointing to your just created repo: we will call it &lt;code&gt;origin&lt;/code&gt;, like the one we have just removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote add origin https://Aerendir@bitbucket.org/Aerendir/web-serendipityhq-wp_theme.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s see what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote
origin
MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote show origin
Password for 'https://Aerendir@bitbucket.org': 
* remote origin
  Fetch URL: https://Aerendir@bitbucket.org/Aerendir/web-serendipityhq-wp_theme.git
  Push URL: https://Aerendir@bitbucket.org/Aerendir/web-serendipityhq-wp_theme.git
  HEAD branch: (unknown)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NOTE: Calling those commands you may be asked for your password: obviously, provide it in the console to access the remote repo. You should set your credentials via SSH to avoid providing the password any time.&lt;/p&gt;

&lt;p&gt;We have a new &lt;code&gt;origin&lt;/code&gt; &lt;code&gt;remote&lt;/code&gt; that now points to our repository.&lt;/p&gt;

&lt;p&gt;The thing to note is that now we have an unknown &lt;code&gt;HEAD&lt;/code&gt; branch: this is because our repository is still empty: we need to push our Genestrap-based theme to it!&lt;/p&gt;

&lt;h3&gt;
  
  
  First push of the Genestrap-based WordPress theme to the &lt;code&gt;git&lt;/code&gt; repository
&lt;/h3&gt;

&lt;p&gt;You need a single, simple command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git push -u origin master
Password for 'https://Aerendir@bitbucket.org': 
Counting objects: 200, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (79/79), done.
Writing objects: 100% (200/200), 100.19 KiB | 0 bytes/s, done.
Total 200 (delta 113), reused 200 (delta 113)
remote: Resolving deltas: 100% (113/113), done.
To https://Aerendir@bitbucket.org/Aerendir/web-serendipityhq-wp_theme.git
 * [new branch] master -&amp;gt; master
Branch master set up to track remote branch master from origin.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this command we have told &lt;code&gt;git&lt;/code&gt; to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push our code on the remote repository;&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;upstream&lt;/code&gt; (the local repository now tracks the branch &lt;code&gt;master&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;The previous two points are executed on the &lt;code&gt;remote&lt;/code&gt; called &lt;code&gt;origin&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Sets the name of the branch to &lt;code&gt;master&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you refresh the page of your repository in the browser, you now see all the files of your Genestrap-based WordPress theme:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XsA_cCxE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/bitbucket-genestrap-theme-pushed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XsA_cCxE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/bitbucket-genestrap-theme-pushed.png" alt="" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Verify again the &lt;code&gt;remote&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote show origin
Password for 'https://Aerendir@bitbucket.org': 
* remote origin
  Fetch URL: https://Aerendir@bitbucket.org/Aerendir/web-serendipityhq-wp_theme.git
  Push URL: https://Aerendir@bitbucket.org/Aerendir/web-serendipityhq-wp_theme.git
  HEAD branch: master
  Remote branch:
    master tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect: now our &lt;code&gt;remote&lt;/code&gt; points to our private &lt;code&gt;git&lt;/code&gt; repository and correctly tracks our &lt;code&gt;master&lt;/code&gt; branch!&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating your custom Genestrap-based WordPress theme
&lt;/h2&gt;

&lt;p&gt;Now that we have &lt;code&gt;git&lt;/code&gt; set up, we can also start to customize the theme.&lt;/p&gt;

&lt;p&gt;Each time you make a modification, commit and push and your remote repository will save the editings.&lt;/p&gt;

&lt;p&gt;But those editings are not reflected on your WordPress site.&lt;/p&gt;

&lt;p&gt;So, here we need to clarify a thing: what does it mean “Updating a Genestrap-based theme”?&lt;/p&gt;

&lt;p&gt;Well, “updating” means three different things with Genestrap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updating the private remote repository (with a &lt;code&gt;push&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;Updating the built Genestrap-based theme on your WordPress site;&lt;/li&gt;
&lt;li&gt;Updating the source code of your Genestrap-based theme FROM the main Genestrap public repository.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one of those “update” requires different actions, so let’s see them in details one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the private remote repository of your Genestrap-based WordPress theme
&lt;/h3&gt;

&lt;p&gt;This is really easy as we already have set up our local repository to push to our remote one.&lt;/p&gt;

&lt;p&gt;So, each time you made an editing, simply commit and then push: done.&lt;/p&gt;

&lt;p&gt;Nothing complex here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the built Genestrap-based theme on your WordPress site
&lt;/h3&gt;

&lt;p&gt;When you want to update the theme on your Genestrap-based theme WordPress site, you need to build it again.&lt;/p&gt;

&lt;p&gt;So, run &lt;code&gt;gulp build&lt;/code&gt; and the theme will be built again.&lt;/p&gt;

&lt;p&gt;Then upload the files in the folder &lt;code&gt;src/build/theme&lt;/code&gt; to the folder of your theme on your hosting and you are done.&lt;/p&gt;

&lt;p&gt;But there are some caveats here.&lt;/p&gt;

&lt;p&gt;If you inspect the code of any of the pages of your WordPress site, in fact, you will notice that &lt;code&gt;css&lt;/code&gt; and &lt;code&gt;js&lt;/code&gt; are imported using a query string parameter, something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link rel="stylesheet" id="genesis-with-bootstrap-css" href="https://io.serendipityhq.com/wp-content/themes/theme/style.css?ver=0.1" type="text/css" media="all"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that if you edit a &lt;code&gt;css&lt;/code&gt; or a &lt;code&gt;js&lt;/code&gt; file it will not be rendered on the page, also if you upload it to your hosting via FTP: this is because the query string parameter is used by the browser to cache the file and load the page faster on next load.&lt;/p&gt;

&lt;p&gt;And this means that you will not see the changes until the browser invalidates the cache (or you empty it).&lt;/p&gt;

&lt;p&gt;So, when editing front-end files, you need to also modify the version of your theme to force the browser to download (and cache again) the new, updated version.&lt;/p&gt;

&lt;p&gt;To change the version number, you need to edit the files &lt;code&gt;style.css&lt;/code&gt; and &lt;code&gt;config.php&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The version in &lt;code&gt;style.css&lt;/code&gt; will be used in the backend to show the current version of the theme;&lt;/li&gt;
&lt;li&gt;The version in &lt;code&gt;config.php&lt;/code&gt; will be used to create the query string parameter for caching purposes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, whenever you edit a &lt;code&gt;css&lt;/code&gt; or an &lt;code&gt;scss&lt;/code&gt; file, you have to update both version numbers to see your changes on your live WordPress site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the source code of your Genestrap-based theme FROM the main Genestrap public repository
&lt;/h3&gt;

&lt;p&gt;From time to time we make some changes to the code of Genestrap to adapt to new versions of Genesis or Bootstrap or to add some functionalities.&lt;/p&gt;

&lt;p&gt;In general, we try to keep Genestrap as thin as possible: this way we can maintain the freedom of both Bootstrap and Genesis.&lt;/p&gt;

&lt;p&gt;However, sometimes, as told, some changes are required.&lt;/p&gt;

&lt;p&gt;Those changes can be made to two kinds of files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;*.dist.*&lt;/code&gt; files;&lt;/li&gt;
&lt;li&gt;Internal files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the first case, you have no other options than updating manually your code, as this kind of file is meant to be customized by any one of us using Genestrap.&lt;/p&gt;

&lt;p&gt;When modifications happen to internal files, instead, it is useful to know how to update your theme to the new version.&lt;/p&gt;

&lt;p&gt;The flow is really simple and involves the use of &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the first part of this post, we removed the &lt;code&gt;remote&lt;/code&gt; &lt;code&gt;origin&lt;/code&gt; that pointed to the public repository of Genestrap on GitHub.&lt;/p&gt;

&lt;p&gt;But that &lt;code&gt;remote&lt;/code&gt; is required to update our local files (and then, also the ones in our remote repository).&lt;/p&gt;

&lt;p&gt;So, we need to add again the remote public repository of Genestrap, but this time, we’ll call the remote exactly &lt;code&gt;genestrap&lt;/code&gt; (just to be original!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote add genestrap https://github.com/Aerendir/Genestrap.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, we now have two remotes: &lt;code&gt;origin&lt;/code&gt; and &lt;code&gt;genestrap&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can check them, too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote
genestrap
origin
MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git remote show genestrap
* remote genestrap
Fetch URL: https://github.com/Aerendir/Genestrap.git
Push URL: https://github.com/Aerendir/Genestrap.git
HEAD branch: master
Remote branch:
master new (next fetch will store in remotes/genestrap)
Local ref configured for 'git push':
master pushes to master (local out of date)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you want to save your modifications to your private repository, you use &lt;code&gt;git push origin master&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To update your theme to the last version of Genestrap, instead, you need to follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;checkout&lt;/code&gt; the last version of Genestrap from GitHub;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;merge&lt;/code&gt; the checked out branch in your master branch;&lt;/li&gt;
&lt;li&gt;Edit the version of your theme if required, commit and push;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;push&lt;/code&gt; the updated &lt;code&gt;master&lt;/code&gt; branch to your remote repository;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gulp build&lt;/code&gt; the theme again (maybe, changing the version number if required) and upload it to your WordPress live site.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  1. &lt;code&gt;checkout&lt;/code&gt; the last version of Genestrap from GitHub
&lt;/h4&gt;

&lt;p&gt;This means running a command like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git checkout -b genestrap-master genestrap/master
Branch genestrap-master set up to track remote branch master from genestrap.
Switched to a new branch 'genestrap-master'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your active branch is &lt;code&gt;genestrap-master&lt;/code&gt; that is the branch &lt;code&gt;master&lt;/code&gt; on the GitHub public repository of Genestrap: at this moment you have the last version of Genestrap.&lt;/p&gt;

&lt;p&gt;Now, you need to update the &lt;code&gt;master&lt;/code&gt; branch of your local repository.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. &lt;code&gt;merge&lt;/code&gt; the checked out branch in your master branch
&lt;/h4&gt;

&lt;p&gt;To merge the branch &lt;code&gt;genestrap-master&lt;/code&gt; to &lt;code&gt;master&lt;/code&gt;, you need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make the &lt;code&gt;master&lt;/code&gt; branch the active one;&lt;/li&gt;
&lt;li&gt;Merge &lt;code&gt;genestrap-master&lt;/code&gt; in &lt;code&gt;master&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In console commands, the procedure is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git merge genestrap-master
Updating d493923..c7534fe
Fast-forward
gulpfile.js | 76 +++++++++++++++++++++++++++++++++++++++++++---------------------------------
1 file changed, 43 insertions(+), 33 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run the above command again, &lt;code&gt;git&lt;/code&gt; will tell you that there is nothing to update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ git merge genestrap-master
Already up-to-date.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Edit the version of your theme
&lt;/h4&gt;

&lt;p&gt;As told before, you have to update the version number of your theme, so the cached files are ignored and the new ones are used by browsers.&lt;/p&gt;

&lt;p&gt;You need to commit also these changes.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. and 5. &lt;code&gt;push&lt;/code&gt; and update
&lt;/h4&gt;

&lt;p&gt;You already know how to do this: you need to push to your remote private git repository the update &lt;code&gt;master&lt;/code&gt; branch (that now contains the modifications from branch &lt;code&gt;genestrap-master&lt;/code&gt;), then rebuild the theme with &lt;code&gt;gulp build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then you can update the theme on your WordPress live site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and next steps
&lt;/h2&gt;

&lt;p&gt;You now have a clear understanding of how to use Genestrap to build a custom theme based on Genesis and Bootstrap for your WordPress site.&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;In the meantime, I wish you flocking users!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/genestrap-development-workflow/"&gt;How to set up the workflow to develop a Genestrap-based WordPress theme&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>bootstrap</category>
      <category>genesis</category>
      <category>genestrap</category>
    </item>
    <item>
      <title>How to create your custom WordPress theme with Genestrap</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Mon, 19 Nov 2018 17:52:37 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-create-your-custom-wordpress-theme-with-genestrap-18oj</link>
      <guid>https://dev.to/serendipityhq/how-to-create-your-custom-wordpress-theme-with-genestrap-18oj</guid>
      <description>&lt;p&gt;Building a WordPress theme from scratch can be a really heavy task.&lt;/p&gt;

&lt;p&gt;But buying a stock theme may not always be an option.&lt;/p&gt;

&lt;p&gt;The result is that you need to develop your theme from scratch.&lt;/p&gt;

&lt;p&gt;And if you would like to use Bootstrap, the task may become very complex very fast.&lt;/p&gt;

&lt;p&gt;Genestrap is the perfect solution: easy, fast and flexible.&lt;/p&gt;

&lt;p&gt;And you can develop seriously, without being a mere WordPress “point-and-clicker”!&lt;/p&gt;

&lt;p&gt;For most of the websites based on WordPress that I manage, I go to one of the stock theme websites, buy a nice theme and install and configure it.&lt;/p&gt;

&lt;p&gt;This is a good way of making a website up and running in a very fast way.&lt;/p&gt;

&lt;p&gt;However, when I had the necessity to build the marketing sites of &lt;a href="https://www.trustback.me"&gt;TrustBack.Me&lt;/a&gt; (marketing, support, ecc.) I decided to use WordPress instead of reinventing the wheel and develop all from scratch with Symfony.&lt;/p&gt;

&lt;p&gt;But buying a stock theme was not an option: making one of them match perfectly the look and feel of TrustBack.me’s app site would be a nightmare, if not impossible at all.&lt;/p&gt;

&lt;p&gt;On TrustBack.Me I used Bootstrap on top of Twig and Symfony, but buying a Bootstrap based stock template would not have given me the flexibility of which Bootstrap is capable of.&lt;/p&gt;

&lt;p&gt;I needed a different solution.&lt;/p&gt;

&lt;p&gt;So I came upon &lt;a href="https://my.studiopress.com/themes/genesis/"&gt;Genesis framework home page&lt;/a&gt;: hey, this seemed the right tool for the job!&lt;/p&gt;

&lt;p&gt;I could have added Bootstrap on top of it, use all the tools that make Bootstrap flexible, I could have continued to feel a developer, and, in the end, have a WordPress site perfectly matched with the app site of TrustBack.Me!&lt;/p&gt;

&lt;h2&gt;
  
  
  Something about Genesis
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pRJxjon9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/Genesis-Framework.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pRJxjon9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/Genesis-Framework.png" alt="" width="700" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Genesis Framework was the foundation on top of which was built the main theme of &lt;a href="https://www.copyblogger.com/"&gt;CopyBlogger&lt;/a&gt;, one of the best places on the Internet to learn about blogging and digital marketing.&lt;/p&gt;

&lt;p&gt;Now it is developed by &lt;a href="https://www.studiopress.com"&gt;StudioPress&lt;/a&gt;, that is in turn managed by &lt;a href="https://wpengine.com/"&gt;WP Engine&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;StudioPress &lt;a href="https://my.studiopress.com/themes/genesis/"&gt;describes&lt;/a&gt; the Genesis Framework this way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Genesis Framework empowers you to quickly and easily build incredible websites with WordPress. Whether you’re a novice or advanced developer, Genesis provides the secure and search-engine-optimized foundation that takes WordPress to places you never thought it could go.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We chose the Genesis Framework as it makes us able to develop our custom WordPress theme, using the tools all the developers like us love (Git, Webpack, Gulp, Yarn or NPM, etc.), maintaining our WordPress very very fast (try to test Serendipity HQ!) and allowing us to continue to use our development workflows.&lt;/p&gt;

&lt;p&gt;To get a full list of the features of Genesis, you can read &lt;a href="https://www.studiopress.com/features/"&gt;this&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Something about Bootstrap
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Y-jzANRL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/bootstrap-framework.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Y-jzANRL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/bootstrap-framework.png" alt="" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://getbootstrap.com/"&gt;Bootstrap is a CSS framework&lt;/a&gt; originally open sourced by Twitter.&lt;/p&gt;

&lt;p&gt;It is the foundation of a lot of websites on the Internet.&lt;/p&gt;

&lt;p&gt;Bootstrap offers out of the box a lot of useful tools to develop the CSS of a website, offering the most used CSS elements.&lt;/p&gt;

&lt;p&gt;The framework is highly customizable and permits to change practically everything on each installation (also if a lot of people simply download the framework as is, without any customization, making their website practically the same of many others).&lt;/p&gt;

&lt;p&gt;We use Bootstrap on TrustBack.me as it permits us to speed up the development of the app and permits us to reach a high level of customization, too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we built Genestrap
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VkQ3dnXs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/Genestrap-min.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VkQ3dnXs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/Genestrap-min.jpg" alt="" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Genestrap, as the name implies, is the fusion of Genesis Framework for WordPress with the Bootstrap CSS Framework.&lt;/p&gt;

&lt;p&gt;We use Genestrap on a lot of our projects, like TrustBack.Me (for the marketing site), here at Serendipity HQ (works also with multisite that we use for Serendipity HQ), on &lt;a href="https://ecommercers.net"&gt;eCommerceRS.NET&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is our foundation for any WordPress site we build internally or for clients when the budget allows us to use it.&lt;/p&gt;

&lt;p&gt;We wanted to use Bootstrap on top of Genesis and as the bootstrapping work is always the same, we decided to ship it as a package to use as the starting point.&lt;/p&gt;

&lt;p&gt;You can find the &lt;a href="https://github.com/Aerendir/Genestrap"&gt;main repository of Genestrap on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to start developing your new WordPress theme based on Genstrap (Bootstrap + Genestrap)
&lt;/h2&gt;

&lt;p&gt;We will use PHPStorm, but if you feel more comfortable, feel free to use the command line alone: we prefer PHPStorm because it will give us a lot of useful features that a command line alone (nor a simple, yet advanced and fully configurable editor like Atom) simply cannot offer.&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 1: Clone locally the Genestrap repository
&lt;/h3&gt;

&lt;p&gt;From PHPStorm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on “Check out from Version Control” and select “Git” (or &lt;code&gt;VCS &amp;gt; Check out from Version Control &amp;gt; Git&lt;/code&gt;): the “Clone Repository” window opens;&lt;/li&gt;
&lt;li&gt;Now set the URL of the Genestrap repository: &lt;code&gt;https://github.com/Aerendir/Genestrap.git&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Set the directory in which to clone the repo (we created a folder named &lt;code&gt;[ProjectName] &amp;gt; [GenestrapTheme]&lt;/code&gt;: &lt;code&gt;SerendipityHQ.com &amp;gt; SHQ-GenestrapTheme&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Click on the “Clone” blue button and PHPStorm will start to clone the repository.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--39Hr0VQI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/genestrap-clone-repo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--39Hr0VQI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/genestrap-clone-repo.png" alt="" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the repository is cloned, PHPStorm will ask you to open the new project: click on “Yes” blue button.&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 2: Create the &lt;code&gt;src/style.css&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;Copy the file &lt;code&gt;src/style.dist.css&lt;/code&gt; in a new file called &lt;code&gt;src/style.css&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the main entry point of the theme: the &lt;a href="https://codex.wordpress.org/Theme_Development#Theme_Stylesheet"&gt;Theme Stylesheet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It will be used by WordPress to get some useful information about your theme and to show a miniature in the list of themes available for your site once you have uploaded your new Genestrap-based theme.&lt;/p&gt;

&lt;p&gt;It is a really simple file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*
   Theme Name: Genestrap
   Theme URI: http://www.serendipityhq.com/
   Description: This is Genestrap, Genesis with Bootstrap
   Author: Aerendir
   Author URI: http://aerendir.me

   Version: 0.1

   Tags: Genesis, Bootstrap

   Template: genesis
   Template Version: 2.3.0

   License: MIT

   Text Domain: shq-genesis-bootstrap
*/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Customize it with your details and the details of your project.&lt;/p&gt;

&lt;p&gt;This is how I customized our Genestrap theme for eCommerceRS.NET:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*
    Theme Name: eCommerceRS
    Theme URI: https://ecommercers.net/
    Description: This is the Genestrap based theme of eCommerceRS.NET.
    Author: Aerendir
    Author URI: http://aerendir.me

    Version: 0.1

    Tags: Genesis, Bootstrap

    Template: genesis
    Template Version: 2.3.0

    License: MIT

    Text Domain: shq-genestrap
*/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  STEP 3: Create the &lt;code&gt;config.php&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Copy the file &lt;code&gt;src/config.dist.php&lt;/code&gt; in a new file called &lt;code&gt;src/config.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This file will set some base constants used by WordPress when showing you the theme in the list of themes.&lt;/p&gt;

&lt;p&gt;It is a really simple file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

/*
 * All the configuration required to configure the theme
 */

return [
   'CHILD_THEME_NAME' =&amp;gt; 'Genestrap',
   'CHILD_THEME_URL' =&amp;gt; 'https://www.serendipityhq.com',
   'CHILD_THEME_VERSION' =&amp;gt; '0.1'
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how I customized the file for eCommerceRS.NET:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

/*
 * All the configuration required to configure the theme
 */

return [
    'CHILD_THEME_NAME' =&amp;gt; 'eCommerceRS',
    'CHILD_THEME_URL' =&amp;gt; 'https://ecommercers.net',
    'CHILD_THEME_VERSION' =&amp;gt; '0.1'
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  STEP 4: Create the &lt;code&gt;functions.php&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Copy the file &lt;code&gt;src/functions.dist.php&lt;/code&gt; in a new file called &lt;code&gt;src/functions.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This file &lt;a href="https://codex.wordpress.org/Theme_Development#Functions_File"&gt;sets some defaults of Genestrap&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 5: Create the &lt;code&gt;src/scss/style.scss&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;Copy the file &lt;code&gt;src/scss/style.dist.scss&lt;/code&gt; in a new file called &lt;code&gt;src/scss/style.scss&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the file that really defines the customizations you will make to make the Genestrap theme really your Genestrap customized theme.&lt;/p&gt;

&lt;p&gt;It basically includes Bootstrap and defines some other custom styles used by Genestrap.&lt;/p&gt;

&lt;p&gt;More about this in a bit.&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 6: Create the &lt;code&gt;src/scss/bootstrap.scss&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;Copy the file &lt;code&gt;src/scss/bootstrap.dist.scss&lt;/code&gt; in a new file called &lt;code&gt;src/scss/bootstrap.scss&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This file defines the Bootstrap features that will be imported in your theme: this way if you don’t need some features of Bootstrap, you can simply exclude them and your resulting &lt;code&gt;css&lt;/code&gt; will be lighter.&lt;/p&gt;

&lt;p&gt;More about this in a bit.&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 7: Install Composer packages
&lt;/h3&gt;

&lt;p&gt;Simply run &lt;code&gt;composer install&lt;/code&gt; to install all the PHP dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ composer install
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 5 installs, 0 updates, 0 removals
- Installing composer/installers (v1.5.0): Downloading (100%)
- Installing squizlabs/php_codesniffer (3.2.3): Downloading (100%)
- Installing wp-coding-standards/wpcs (0.14.1): Downloading (100%)
- Installing stevegrunwell/wp-enforcer (v0.5.0): Downloading (100%)
- Installing wp-bootstrap/wp-bootstrap-navwalker (v4.0.2): Downloading (100%)
wp-coding-standards/wpcs suggests installing dealerdirect/phpcodesniffer-composer-installer (^0.4.3)
Generating autoload files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is required as, among the others, the dependency is&lt;code&gt;wp-bootstrap/wp-bootstrap-navwalker&lt;/code&gt; installed: it is required to make the menus work well and is copied in the theme’s package when you build it (we will see in a bit how to do this).&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 8: Install Javascript dependencies
&lt;/h3&gt;

&lt;p&gt;You can use both &lt;a href="https://www.npmjs.com/"&gt;NPM&lt;/a&gt; or &lt;a href="https://yarnpkg.com/"&gt;Yarn&lt;/a&gt; to install the Javascript dependencies: we prefer Yarn, so our example will use it.&lt;/p&gt;

&lt;p&gt;Simply run the command &lt;code&gt;yarn install&lt;/code&gt; to install the Javascript dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ yarn install
yarn install v1.7.0
info No lockfile found.
(node:22378) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
[1/4] ? Resolving packages...
warning gulp-sass &amp;gt; gulp-util@3.0.8: gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5
warning popper &amp;gt; rijs.sync &amp;gt; xrs &amp;gt; uws@9.148.0: stop using this version
[2/4] ? Fetching packages...
[3/4] ? Linking dependencies...
warning " &amp;gt; bootstrap@4.0.0" has unmet peer dependency "popper.js@^1.12.9".
[4/4] ? Building fresh packages...
success Saved lockfile.
warning Your current version of Yarn is out of date. The latest version is "1.9.4", while you're on "1.7.0".
info To upgrade, run the following command:
$ brew upgrade yarn
 Done in 44.88s.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, I need to update Yarn on my machine. ?&lt;/p&gt;

&lt;p&gt;Anyway, you now have all the required Javascript dependencies installed: they are required to build the theme being able to upload it to your WordPress website.&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 9: Building the theme
&lt;/h3&gt;

&lt;p&gt;Before starting customizing the theme, let’s finish our round up to understand the complete flow: later I will explain you how to customize it, too.&lt;/p&gt;

&lt;p&gt;So, the next step is to build our theme.&lt;/p&gt;

&lt;p&gt;The building process will create a folder ready to be compressed and uploaded where you like: directly in a WordPress site or on a server to make the theme downloadable by all WordPress sites that use it.&lt;/p&gt;

&lt;p&gt;To build the theme, simply run &lt;code&gt;gulp build&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook-Pro-di-Aerendir:GenestrapTheme Aerendir$ gulp build
[17:59:08] Using gulpfile ~/Documents/JooServer/_Projects/SerendipityHQ/SerendipityHQ.com/GenestrapTheme/gulpfile.js
[17:59:08] Starting 'build'...
[17:59:08] Starting 'clean-build'...
[17:59:08] Finished 'clean-build' after 3.5 ms
[17:59:08] Starting 'build-css'...
[17:59:08] Finished 'build-css' after 18 ms
[17:59:08] Starting 'move-bootstrap-bundle'...
[17:59:08] Finished 'move-bootstrap-bundle' after 3.13 ms
[17:59:08] Starting 'move-php-files'...
[17:59:08] Finished 'move-php-files' after 7.67 ms
[17:59:08] Starting 'move-bootstrap-walker'...
[17:59:08] Finished 'move-bootstrap-walker' after 1.42 ms
[17:59:08] Finished 'build' after 39 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see from the stopwatching, it requires a blink of an eye to complete.&lt;/p&gt;

&lt;p&gt;When building is done, check the folder &lt;code&gt;build&lt;/code&gt;: here you will find the built theme.&lt;/p&gt;

&lt;h3&gt;
  
  
  STEP 10: Compress, upload and activate your Genestrap based theme
&lt;/h3&gt;

&lt;p&gt;At this point your theme is ready to be compressed: create an archive, then upload it to your WordPress website.&lt;/p&gt;

&lt;p&gt;So, go to &lt;code&gt;Appearance &amp;gt; Themes &amp;gt; Add theme &amp;gt; Upload file&lt;/code&gt; and upload:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Genesis Framework;&lt;/li&gt;
&lt;li&gt;Then, the just created archive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your list of themes will appear similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--F2Rr-Duh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/genestrap-installed-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F2Rr-Duh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/genestrap-installed-min.png" alt="" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the image, the theme is called “&lt;a href="https://www.trustback.me"&gt;TrustBack.Me&lt;/a&gt;” as we developed it to manage its marketing site: in a bit, you will be able to customize its name, trust me!&lt;/p&gt;

&lt;p&gt;Now, the only thing left to do is to activate the theme and go to the home page of your WordPress site to check it.&lt;/p&gt;

&lt;p&gt;The result will be similar to this (obviously, with different posts!):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--i3_QlqR8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/genestrap-activated.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i3_QlqR8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/09/genestrap-activated.png" alt="" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and next steps
&lt;/h2&gt;

&lt;p&gt;Creating a working theme with Genestrap is really an easy task: nothing more complex of creating a WordPress theme from scratch, but with the more heavy work already done for you.&lt;/p&gt;

&lt;p&gt;Now it is time to do the next step: &lt;a href="https://io.serendipityhq.com/experience/genestrap-development-workflow/"&gt;Implement a development workflow&lt;/a&gt;, so you can save your customized Genestrap-based WordPress theme and collaborate with others to improve it.&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/wordpress-custom-theme-with-genestrap/"&gt;How to create your custom WordPress theme with Genestrap&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>bootstrap</category>
      <category>genesis</category>
      <category>genestrap</category>
    </item>
    <item>
      <title>How to track unique users with behavioural analytics tools</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Sat, 17 Nov 2018 10:42:02 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-track-unique-users-with-behavioural-analytics-tools-2oi3</link>
      <guid>https://dev.to/serendipityhq/how-to-track-unique-users-with-behavioural-analytics-tools-2oi3</guid>
      <description>&lt;p&gt;The identification of users is the second pillar of behavioural tracking, together with the understanding of events.&lt;/p&gt;

&lt;p&gt;Identifying users precisely makes you able to track the complete journey of any user to understand why exactly (s)he signed up or goes away, making you able to uncover hidden causes that you can improve or use to improve your KPIs and your bottom line.&lt;/p&gt;

&lt;p&gt;But the identification of users is not so simple as it may seem at a first look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it mean “Identify customers”
&lt;/h2&gt;

&lt;p&gt;This is something you simply cannot do with Google Analytics as &lt;a href="https://support.google.com/analytics/answer/6366371?hl=en"&gt;it explicitly prohibits you to do this kind of profiling&lt;/a&gt; as it has deep GDPR implications (of which we will speak later in another post).&lt;/p&gt;

&lt;p&gt;This is the core functionality of each behavioural analytics tool: identify customers and their journeys using your app.&lt;/p&gt;

&lt;p&gt;This is logically the next step in your journey ( &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt; ) to instrumenting tracking on your SAAS application.&lt;/p&gt;

&lt;p&gt;Identifying users is a complex thing: many scenarios may happen in the real world, and to some of them maybe you didn’t think too much.&lt;/p&gt;

&lt;p&gt;Amplitude &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115003135607"&gt;sums up the troubles&lt;/a&gt; very well:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tracking unique users is a complex process because users can log in and out of your product, browse anonymously, and use multiple devices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also offers a good starting list of possible scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No User ID is assigned;&lt;/li&gt;
&lt;li&gt;The user ID is assigned after anonymous events;&lt;/li&gt;
&lt;li&gt;Same User ID on multiple Device IDs, with no anonymous events;&lt;/li&gt;
&lt;li&gt;Multiple User IDs on the same Device ID;&lt;/li&gt;
&lt;li&gt;Same User ID on multiple Device IDs, with anonymous events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As told, this is only a starting list and Amplitude explains them in detail in the linked page.&lt;/p&gt;

&lt;p&gt;In general, the track happens for users and for devices.&lt;/p&gt;

&lt;p&gt;Woopra explains, too, the &lt;a href="https://docs.woopra.com/docs/profile-id-system"&gt;challenges we will face in understanding who did what on our website or in our native app&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Woopra system needs to be able to tell which person profile performed an incoming tracked action (or property update.) The problem is that people in Woopra can exist in a number of different levels of being identified. They could be a first-time anonymous visitor to a website, or a long time paying customer.&lt;/p&gt;

&lt;p&gt;Sometimes a person will make a few visits to your site anonymously over a year leading up to the time they decide to sign up for your newsletter, giving you their email. Sometimes this can even mean that what was previously considered to be two different people in Woopra, is now known to be a single person–perhaps originally from two devices–requiring a merge of the two profiles.&lt;/p&gt;

&lt;p&gt;[…]&lt;/p&gt;

&lt;p&gt;Another issue is that if you want to track anonymous behavior, and even attribute it to known people in the future as they identify themselves to you–a key value proposition in the Woopra system–then using a single id value per person become more complex.&lt;/p&gt;

&lt;p&gt;Similarly, if you want to track behavior across channels–another key value proposition of Woopra–then it is basically impossible to maintain the database ID for the profile between your website, and, say, your email marketing automation service.&lt;/p&gt;

&lt;p&gt;[…]&lt;br&gt;&lt;br&gt;
&lt;strong&gt;The Goal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Woopra needs to be able to take whatever information is available about a person performing the action in an incoming track request, and use it to determine with the highest accuracy possible, which other actions this person has performed, and thus to which profile the incoming actions belong.&lt;/p&gt;

&lt;p&gt;If a user is coming anonymously to your website, all you have is a cookie, which is conceptually, a device ID pointing to that browser on that machine. It will be the same next time the person visits your site from that machine and that browser, but if they visit from a different browser, or on their phone, for instance, you will have a new cookie. So Woopra needs to be able to use multiple cookies to eventually refer to one person, assuming that one day you find out who that person is and can associate all their devices with them.&lt;/p&gt;

&lt;p&gt;Similarly, you may have an incoming “Email Sent” event from your email marketing tool that is not from a browser and has no cookie. This event has an email address–another major identifier. Woopra needs to eventually (when the person signs in with that email address on their browser with that cookie they had in the past) be able to consider the actions performed by cookie 1, cookie 2, and email 1 all to belong to the same profile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, the edge combinations are&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same user on different devices on one side;&lt;/li&gt;
&lt;li&gt;Different users on the same device on the other side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A third factor to take into consideration in SAAS products is the domain: in fact, very probably, you will have at least two different domains, one for the application itself, one for the documentation, the marketing pages, etc.&lt;/p&gt;

&lt;p&gt;So, the three factors to consider are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device identification;&lt;/li&gt;
&lt;li&gt;Domain navigated;&lt;/li&gt;
&lt;li&gt;User identification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wanting to be extremely synthetic, the real meaning of “user identification”, after all, is associating a user session with a single person and only one, or a sequence of sessions, or more sequences of sessions – oh damn, how complex is it? ? -, also cross devices ? and cross domains. ?&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do you need to identify users one-by-one
&lt;/h3&gt;

&lt;p&gt;As it is so complex to identify users one-by-one, it is helpful to understand the “Why”: why are we lavish all these energies in something so complex?&lt;/p&gt;

&lt;p&gt;The answer is simple: knowledge!&lt;/p&gt;

&lt;p&gt;The Customer is the foundation of every business on the Earth and knowing them better than themselves is fundamental to grow your business.&lt;/p&gt;

&lt;p&gt;Knowing them with such degree of details opens up possibilities that you do not even imagine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can understand exactly why each of your customers didn’t upgrade to a premium feature;&lt;/li&gt;
&lt;li&gt;You can understand which have in common the customers who upgraded to a premium feature;&lt;/li&gt;
&lt;li&gt;You can understand which features are most used by which customers and what they have in common;&lt;/li&gt;
&lt;li&gt;You can understand why a customer downgraded (maybe (s)he didn’t found a helpful article in your knowledge base after having navigated it for a lot of time?);&lt;/li&gt;
&lt;li&gt;You can send custom messages to them (MixPanel has a &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004501966-People-Profiles"&gt;built-in tool&lt;/a&gt; for this).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Obviously, those are simple examples: the limitations are tied only to the detail degree of your implementation, your imagination and the current necessities of your application.&lt;/p&gt;

&lt;p&gt;Using the words of Amplify, &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/206404628-Step-2-Assign-User-IDs-and-Identify-Your-Users#assigning-user-ids"&gt;identifying users concretely means this&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Products that have some kind of login system can track users even if they switch devices. Though assigning User IDs is optional, we recommend that products with a login system or a UUID (unique user identifier) system assign a User ID.&lt;/p&gt;

&lt;p&gt;With a User ID, Amplitude can match events across multiple devices under the same user (same User ID). Furthermore, a User ID does not need to be assigned immediately. A user’s event data will be merged on the backend so that all anonymous events up to the point of User ID assignment will be connected to the assigned User ID (assuming the Device ID is consistent).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Basic concepts of Device Identification
&lt;/h3&gt;

&lt;p&gt;All the tools that track users use the same identical approach.&lt;/p&gt;

&lt;p&gt;There are basically two main categories of devices on which you track users:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A browser&lt;/li&gt;
&lt;li&gt;A native application.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115003135607#determining-unique-users"&gt;Tracking Unique Users&lt;/a&gt;, Amplitude explains in details how the device identification happens.&lt;/p&gt;

&lt;p&gt;This is what they say about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Device ID:&lt;/strong&gt;  We will pull the IDFV or generate a random alphanumeric string that ends with the letter ‘R’ for Device ID and is stored locally in the browser’s cookie or mobile device. However, there is a flag that you can toggle to use the Identifier for Advertiser (IDFA for iOS) and the Advertising Identifier (AdID for Android) as the Device ID.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;iOS: If the Device ID is set to the IDFA, then it will persist across installs.
&lt;em&gt;Note: iOS users have the option of resetting their IDFA on their devices at any time. As of iOS 10, if a user limits ad tracking, then this would send an IDFA of all zeros. Amplitude will instead set Device ID as IDFV or a randomly generated string.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Android: If the Device ID is the AdID, then it will persist across installs.
&lt;em&gt;Note: Android users have the option of resetting the AdID on their devices at any time.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Web: The Device ID will be set to a randomly generated UUID by default. It will persist unless a user clears their browser cookies and/or is browsing in private mode.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, let’s recap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IDFV: IDentifier For Vendor.&lt;/strong&gt;  This is a unique identifier &lt;a href="https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor"&gt;available only on Apple devices&lt;/a&gt;. Used for tracking in-app. This can be changed to a new one by the user if they want to, but only if they actively change it. This remains the same if the user, for example, resets the device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IDFA: IDentifier For Advertiser.&lt;/strong&gt; This is a unique identifier &lt;a href="https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier"&gt;available only on Apple devices&lt;/a&gt;. Now it is called “Advertiser IDentifier”, but this is practically irrelevant as the behaviour stays the same. Used for tracking in-app. This ID changes if the user resets the phone. It is equal to a series of zeroes if the user opts out from the tracking. If received as all zeroes, the tracking software generates a random string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AdID: Advertiser IDentifier.&lt;/strong&gt;  This is a unique identifier &lt;a href="http://www.androiddocs.com/google/play-services/id.html"&gt;available only on Android devices&lt;/a&gt;. Used for tracking in-app. If the user opts out from the tracking, this ID is a simple random string that changes every time, making the tracking impossible.&lt;/p&gt;

&lt;p&gt;So, in conclusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tracking in web browsers is done through a random string set in cookies;&lt;/li&gt;
&lt;li&gt;the tracking in native apps is done using the identifiers provided by the vendors of the device (or a random string if the identifier is not available).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;IDFV, IDFA, AdID or a random string stored in a cookie are used to uniquely identify any single device from which your site is being visited or your app is being used.&lt;/p&gt;

&lt;p&gt;Those identifiers can change over time if the user decides to change them or if (s)he doesn’t want to be tracked anymore by anyone.&lt;/p&gt;

&lt;p&gt;This means that if they are anonymous (you haven’t identified them) you cannot track them if they don’t want you to do (technically you continue to track, but you have a lot of unique visitors also if concretely you have only one visitor).&lt;/p&gt;

&lt;p&gt;But, once they identify themselves on your system, you can start tracking them again using the User ID you set and associate with their tracking profile the previous anonymous events they fired (More about the User ID – and other ways of identifying users – in a bit).&lt;/p&gt;

&lt;p&gt;Coming to names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amplitude call this ID “&lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115003135607#determining-unique-users"&gt;Device ID&lt;/a&gt;“;&lt;/li&gt;
&lt;li&gt;MixPanel call this ID “&lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004509426-Distinct-ID-Creation-JavaScript-iOS-Android-"&gt;Distinct ID&lt;/a&gt;” (read &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004509406-Distinct-IDs-"&gt;also this&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;Woopra calls this with various names, &lt;a href="https://docs.woopra.com/docs/profile-id-system#section-the-id-field-identifiers-and-cv_id-a-quick-glossarial-note"&gt;depending on the context&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Basic concepts of cross-domain User identification
&lt;/h3&gt;

&lt;p&gt;Identifying unique users across your domains is crucial as this permits you to follow the journey of your users between all your properties, making you able to understand if a user, for example, becomes a customer after (s)he read a piece on your support site.&lt;/p&gt;

&lt;p&gt;It is common for a SAAS product to have more than one domain or more than one subdomain.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.trustback.me"&gt;TrustBack.Me&lt;/a&gt;, for example, we have the main app on &lt;a href="http://www.trustback.me"&gt;www.trustback.me&lt;/a&gt; while other subdomains like aiuto.trustback.me or ciao.trustback.me contain the documentation or the marketing site.&lt;/p&gt;

&lt;p&gt;The same we do also here at SerendipityHQ.com: io.serendipityhq.com contains posts about our R&amp;amp;D, &lt;a href="http://www.serendipityhq.com"&gt;www.serendipityhq.com&lt;/a&gt; contains our marketing site and there are other sub-domains that contain other kinds of information.&lt;/p&gt;

&lt;p&gt;Woopra explains very well the &lt;a href="https://docs.woopra.com/docs/cross-domain-tracking"&gt;importance of cross-domain tracking&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The importance of cross-domain tracking is to not only store your data in a single project, but to also maintain a visitors session when they explore unique domains. The events of previously identified visitors should be monitored and associated with the same customer profile, regardless of the domain they visit. This is the key to providing a comprehensive customer journey.&lt;/p&gt;

&lt;p&gt;When a visitor visits an alternate domain, the cookies associated with that visitor are inaccessible, which causes split profiles in your Woopra project. Without the cookie to associate to the visitor, Woopra has no way of knowing that the visitor on the new domain is the same as the visitor from the previous domain. Woopra would require the visitor to again identify themselves by signing up to an email list or logging in to the platform.&lt;/p&gt;

&lt;p&gt;This is the problem that cross-domain tracking solves. When configured correctly, Woopra will attach the cookie value to the url of all specified domains, allowing it to associate that cookie with the visitor from your previous domain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The main concepts are those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use only one project to track all domains of interest;&lt;/li&gt;
&lt;li&gt;Pass the User ID from one domain to the other using the query string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Really simple.&lt;/p&gt;

&lt;p&gt;Here is the relevant information about cross-domain tracking with each of the three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115003135607#cross-domain%C2%A0tracking"&gt;Amplitude cross-domain tracking&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004511006-Track-Users-Across-Domains"&gt;MixPanel cross-domain tracking&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.woopra.com/docs/cross-domain-tracking"&gt;Woopra cross-domain tracking&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding when a User is identified
&lt;/h2&gt;

&lt;p&gt;The first thing you should understand when thinking of Users’ identification is to find the exact moment in your app that permits you to be sure the User is a specific person.&lt;/p&gt;

&lt;p&gt;The starting point is always your app as, after all, is your app that identifies Users and so is its responsibility to tell the tracking tools when it finds the person behind the data collected.&lt;/p&gt;

&lt;p&gt;At a first look, you may think that your identification happens when you have an email.&lt;/p&gt;

&lt;p&gt;But this is not always the case.&lt;/p&gt;

&lt;p&gt;If you think deeper at the email, you realize that this is a variable ID: practically every application, in fact, permits to change the email address.&lt;/p&gt;

&lt;p&gt;This means that you cannot rely on it to track a user across sessions, domains and devices, as, if (s)he changes the email in your app, then you lose the link between subsequent events (s)he performs in your app and all the previous ones, making the reporting difficult in the best cases, impossible in the worst ones.&lt;/p&gt;

&lt;p&gt;So, you should not rely on email for identification.&lt;/p&gt;

&lt;p&gt;Instead, you should use your unique ID: when you create a new user in your app, you always assign it a new unique ID: this is the identifier you have to use.&lt;/p&gt;

&lt;p&gt;If your app doesn’t have a unique ID assigned to the users (for whatever reason), then the things get a bit more complex and sometimes you have to simply deal with this fact and take it into consideration when reporting and analyzing data.&lt;/p&gt;

&lt;p&gt;Amplitude offers two important &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/206404628-Step-2-Assign-User-IDs-and-Identify-Your-Users#important-recommendations-on-setting-user-ids"&gt;recommendations about setting User IDs&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not set User ID if there isn’t one.&lt;/strong&gt;  For example, setting a User ID to the string ‘None’ to multiple users will group all events under that ‘None’ User ID together (e.g. any user with the ‘None’ User ID is assumed to be the same single user). You can always set the User ID later, and Amplitude has a built-in logic that will merge the anonymous events to the later identified user (see &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115003135607#example-2-user-id-is-assigned-after-anonymous-events"&gt;Example 2&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not assign a User ID that might change.&lt;/strong&gt;  If someone’s email can change within your app, then it is not a good idea to set it as a User ID as Amplitude will mark the person as a new user if they change their email.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;This told, let’s see which is the default user journey that makes him/her pass from the status of “anonymous visitor” to the status of “identified user”.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easiest user’s journey: no cross-domain nor cross-device tracking
&lt;/h2&gt;

&lt;p&gt;Let’s start simple and consider the simplest journey that any user follows on any app or website:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lands on one of the pages of the website or opens the app;&lt;/li&gt;
&lt;li&gt;Signs up;&lt;/li&gt;
&lt;li&gt;Logs in;&lt;/li&gt;
&lt;li&gt;Uses the website or the app;&lt;/li&gt;
&lt;li&gt;Logs out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the default journey of any user on any app or website.&lt;/p&gt;

&lt;p&gt;Nothing complex to understand here: we are considering the use of a single device and the navigation of a single domain/app. No cross-device or cross-domain tracking.&lt;/p&gt;

&lt;p&gt;We will start with this scenario analyzing how it is handled in each of the three tools we are considering.&lt;/p&gt;

&lt;p&gt;We will call this the “Default User’s journey”.&lt;/p&gt;

&lt;p&gt;Once we will have understood how to handle this Default User’s Journey, then we will go one step further and will analyze how to take into consideration cross-domain and cross-device tracking.&lt;/p&gt;

&lt;h3&gt;
  
  
  What do we want to track in the Default User’s Journey
&lt;/h3&gt;

&lt;p&gt;Before making our hands dirty, let’s better understand what we are trying to accomplish.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We want to create a user profile on our tracking tool once the Visitor signs up (step 2);&lt;/li&gt;
&lt;li&gt;We want to link with this user profile all the events (s)he fired while (s)he was anonymous (step 1);&lt;/li&gt;
&lt;li&gt;We want to track all subsequent events, linking them to the User profile we created (step 3 and 4);&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MixPanel has a &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004497803-Identity-Management-Best-Practices"&gt;great illustration that explains exactly what we are trying to accomplish&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hi8naFNY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/default-user-journey-mixpanel-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hi8naFNY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/default-user-journey-mixpanel-min.png" alt="" width="700" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will leave apart the last step: login. In fact, this requires to explain some more things and for the moment we want to leave things easy.&lt;/p&gt;

&lt;p&gt;We will address the login problem later.&lt;/p&gt;

&lt;p&gt;Let’s start!&lt;/p&gt;

&lt;p&gt;Note: All the three tools offer a wide variety of SDKs to make you able to integrate your app into whatever language it is written in.&lt;br&gt;&lt;br&gt;
In those examples, we will use the Javascript SDKs as the examples take into consideration the implementation in a web app.&lt;br&gt;&lt;br&gt;
Anyway, the examples can be adapted to be used in any other language.&lt;/p&gt;
&lt;h3&gt;
  
  
  How to track the Default User’s Journey in Amplitude
&lt;/h3&gt;

&lt;p&gt;In the Javascript Documentation of the Amplitude’s SDK, there is a section named &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115001361248#setting-custom-user-ids"&gt;Setting Custom User IDs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The process to identify a User is really simple: use the &lt;code&gt;setUserId('USER_ID')&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amplitude.getInstance().setUserId('USER_ID');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to track the Default User’s Journey in MixPanel
&lt;/h3&gt;

&lt;p&gt;MixPanel has an &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004510946-Identity-Management-Overview-Video"&gt;overview video&lt;/a&gt; you can watch to understand how generally the identification of visitors works on their platform.&lt;/p&gt;

&lt;p&gt;MixPanel has two main methods for identifying visitors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mixpanel.identify()&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://mixpanel.com/help/reference/javascript-full-api-reference#mixpanel.alias"&gt;&lt;code&gt;mixpanel.alias()&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the moment we will ignore the second method &lt;code&gt;mixpanel.alias()&lt;/code&gt;: we will use it later in this post, when we will understand how works in MixPanel the merging of users and visitors.&lt;/p&gt;

&lt;p&gt;So, for the moment, we will focus only on the first method: &lt;code&gt;mixpanel.identify()&lt;/code&gt;, also if the example from MixPanel uses the second one, too.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to track the Default User’s Journey in Woopra
&lt;/h3&gt;

&lt;p&gt;In the Javascript Documentation of Woopra, they say &lt;a href="https://docs.woopra.com/reference#section-identifying-customers"&gt;we need to call the method &lt;code&gt;identify()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But there are some things to consider:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In order to identify a customer, you need to send their &lt;code&gt;email&lt;/code&gt; address to Woopra as a custom visitor property. Calling &lt;code&gt;identify()&lt;/code&gt; does not send anything to Woopra, in order to send the properties and identify the visitor, you need to call &lt;code&gt;track()&lt;/code&gt; after you &lt;code&gt;identify()&lt;/code&gt;, or if you do not wish to track an event, you may call &lt;code&gt;woopra.push()&lt;/code&gt; to send a call strictly with identifying information.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Woopra, too, as Mixpanel did, suggests us to use the email as the identifier: we continue to prefer to use our internal UUID and send the email attached as a User property (more on this debate later).&lt;/p&gt;

&lt;h3&gt;
  
  
  How to concretely track the users: code example
&lt;/h3&gt;

&lt;p&gt;Now that we know how to identify a user with each of the three tools, let’s make our hands dirty and identify users.&lt;/p&gt;

&lt;p&gt;But… there is a thing to consider first: how is your login system structured?&lt;/p&gt;

&lt;p&gt;Maybe you have not thought at this too much, but if you do, you can discover that there are three main flows for the login:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;With an intermediate logged in page (where you say something like “Hello [User], you are now logged in. We are redirecting you to [page requested | you user profile]”);&lt;/li&gt;
&lt;li&gt;Without an intermediate logged in page BUT WITH a refresh;&lt;/li&gt;
&lt;li&gt;Without an intermediate logged in page AND WITHOUT a refresh (Single Page Applications – SPA).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do you understand what I’m saying?&lt;/p&gt;

&lt;p&gt;The first flow, shows you a form (on a dedicated login page or in a form on any page): once you have provided your credentials, the system first redirects you to another page where it shows you a “You are now logged in message” and then, maybe, it redirects you to the page you were browsing or to your profile.&lt;/p&gt;

&lt;p&gt;The second flow shows you a form for the login: if it is on the page you are browsing (maybe in the top-right part of the page), it refreshes the page and logs you in; if you log in from a dedicated page, it usually redirects you to your profile or main dashboard.&lt;/p&gt;

&lt;p&gt;The third flow has any of the steps of the previous flows: whether you use a form on the browsed page or a dedicated page, no refresh happens as all is handled in front-end by a JS library like React or Angular that simply make the call to the login API you implemented and then, once the login is confirmed, update the DOM, all without any refresh nor redirect.&lt;/p&gt;

&lt;p&gt;Why are we speaking about this?&lt;/p&gt;

&lt;p&gt;Because each flow has its own implementation details: the only constant is the fact that you have to call the method to identify the user.&lt;/p&gt;

&lt;p&gt;So, starting from the first flow, this is the easiest to track: simply call the method &lt;code&gt;identify()&lt;/code&gt; in the page that confirms the login.&lt;/p&gt;

&lt;p&gt;Jumping to the third flow, this case is easy, too: before you update the DOM you need to call the methods to identify the user.&lt;br&gt;&lt;br&gt;
When exactly you call those methods depends on your concrete implementation and framework of choice: unfortunately, I cannot help you more than this.&lt;/p&gt;

&lt;p&gt;The most tedious approach to track is the second: no intermediate page and a refresh.&lt;/p&gt;

&lt;p&gt;This is the most problematic as you practically have no concrete page where to call the the methods to identify the visitor: after the click on the “Login” button, the user is redirected to another page, but usually this doesn’t render anything and simply send a header to the browser to redirect it to the page from which the “login” button was clicked.&lt;/p&gt;

&lt;p&gt;There are many possible ways to identify the user that logs in following this flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call the methods to identify the users on all pages by simply checking if the user is logged in or not: if (s)he is, then identify him/her;&lt;/li&gt;
&lt;li&gt;Call the &lt;code&gt;identify()&lt;/code&gt; method using the server-side SDKs: the controller that logs in the user, other than set the cookie, calls the APIs of the tracking software, too;&lt;/li&gt;
&lt;li&gt;Use a query string parameter to mark a page visited just after a login;&lt;/li&gt;
&lt;li&gt;Use the session to store a parameter to read on successive page load: if present, then identify the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first approach is horrible and I have no intention of talking about it further.&lt;/p&gt;

&lt;p&gt;The second one is possible, but it requires another dependency in your server-side code base, probably a break of the SOLID principle (unless you use events, but this means other unit tests, more code to maintain, handling possible failures in communicating with the tracking APIs, possible delays waiting a response and the list can continue).&lt;/p&gt;

&lt;p&gt;The third can be easy to implement, but if the user refreshes the page, it will be identified again: nothing so bad, but anyway a duplication.&lt;/p&gt;

&lt;p&gt;TrustBack.Me is built on top of the Symfony framework and it has a convenient feature called &lt;a href="https://symfony.com/doc/current/controller.html#flash-messages"&gt;flash messages:&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can also store special messages, called “flash” messages, on the user’s session. By design, flash messages are meant to be used exactly once: they vanish from the session automatically as soon as you retrieve them. This feature makes “flash” messages particularly great for storing user notifications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This really seems the best approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It is clean;&lt;/li&gt;
&lt;li&gt;Doesn’t have refresh issues of doubled identifications;&lt;/li&gt;
&lt;li&gt;Automatically handled by Symfony;&lt;/li&gt;
&lt;li&gt;No stress.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s use it!&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 1: Create the flash message after the user logs in
&lt;/h4&gt;

&lt;p&gt;To create the flash message we need to create a subscriber that listens for the event &lt;code&gt;security.interactive_login&lt;/code&gt;: &lt;a href="https://symfony.com/doc/current/components/security/authentication.html#authentication-events"&gt;during the login, in fact, Symfony fires some events&lt;/a&gt; that we can listen for.&lt;/p&gt;

&lt;p&gt;We need to &lt;a href="https://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events"&gt;use this as&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a provider authenticates the user, a &lt;code&gt;security.authentication.success&lt;/code&gt; event is dispatched. But beware – this event will fire, for example, on &lt;em&gt;every&lt;/em&gt; request if you have session-based authentication. See &lt;code&gt;security.interactive_login&lt;/code&gt; below if you need to do something when a user &lt;em&gt;actually&lt;/em&gt; logs in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, this is our subscriber:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// src/Subscriber/LoginAnalyticsSubscriber.php

&amp;lt;?php

declare(strict_types=1);

/*
 * This file is part of the Trust Back Me Www.
 *
 * Copyright Adamo Aerendir Crespi 2012-2019.
 */

namespace App\Subscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;

/**
 * Subscribes to the event `security.interactive_login` and
 * sets a flash message that marks the login.
 *
 * This way, in Twig, we can check for the existence of this
 * flash message and identify the user in analytics tools.
 */
class LoginAnalyticsSubscriber implements EventSubscriberInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents():array
    {
        return [
            SecurityEvents::INTERACTIVE_LOGIN =&amp;gt; 'onInteractiveLogin',
        ];
    }

    /**
     * Sets the message in the flash bag.
     *
     * @param InteractiveLoginEvent $event
     */
    public function onInteractiveLogin(InteractiveLoginEvent $event):void
    {
        /** @var Session $session */
        $session = $event-&amp;gt;getRequest()-&amp;gt;getSession();
        $session-&amp;gt;getFlashBag()-&amp;gt;add('identify', 'identify');
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Read the flash message in the Twig template
&lt;/h4&gt;

&lt;p&gt;In our template we use this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="{{ locale }}"&amp;gt;
    &amp;lt;head&amp;gt;
        ...

        {% if is_granted('IS_AUTHENTICATED_FULLY') or is_granted('IS_AUTHENTICATED_REMEMBERED') %}
            {% for label, messages in app.flashes(['identify']) %}
                {% for message in messages %}
                    &amp;lt;script type="text/javascript"&amp;gt;
                        console.log('identifying the user.')
                        amplitude.getInstance().setUserId('{{ app.user.id }}');
                        mixpanel.identify('{{ app.user.id }}');
                        woopra.identify({id: '{{ app.user.id }}'});
            amplitude.getInstance().logEvent('Login');
                        mixpanel.track('Login');
                        woopra.track('Login');
                    &amp;lt;/script&amp;gt;
                {% endfor %}
            {% endfor %}
        {% endif %}
    &amp;lt;/head&amp;gt;
    &amp;lt;body class="site"&amp;gt;
        ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, after having set the User ID I also immediately log a “Login” event.&lt;/p&gt;

&lt;p&gt;This is for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It’s a good thing to track the logins as this gives you the measure of the engagement of users with your app;&lt;/li&gt;
&lt;li&gt;You can see Users profiles only after having sent at least one event after the ID setting (more on this in a bit).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note that I’ve inserted a &lt;code&gt;console.log()&lt;/code&gt; message to have a feedback in the Javascript Console: I will obviously remove it in production, once I’ve checked the user identification works as expected.&lt;/p&gt;

&lt;p&gt;Now that we have our identification in place, let’s go to see what happens in Amplitude, MixPanel and Woopra.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens in Amplitude when a user is identified
&lt;/h3&gt;

&lt;p&gt;In Amplitude, Click on the “New” blue button&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--W7TIkRvR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--W7TIkRvR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-min.png" alt="" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click on “User look-up”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iVLS-nw4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-new-user-look-up-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iVLS-nw4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-new-user-look-up-min.png" alt="" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Et voilà&lt;/em&gt;, here there is the list of all your users&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WMuHJLjZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-new-user-look-up-users-list-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WMuHJLjZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-new-user-look-up-users-list-min.png" alt="" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you click on the ID of any user, you will see his/her details&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YiH5cobl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-new-user-look-up-users-details-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YiH5cobl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-new-user-look-up-users-details-min.png" alt="" width="800" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, all the previous events I triggered in the app, before I logged in, are now merged with my profile.&lt;/p&gt;

&lt;p&gt;This means that, once you identify a User, you can see what (s)he did on your app, so rebuilding his/her full journey.&lt;/p&gt;

&lt;p&gt;You can find more information about the User activity tracking in the &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/229313067-User-Activity"&gt;dedicated page in the Amplitude documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens in Woopra when a user is identified
&lt;/h3&gt;

&lt;p&gt;In Woopra:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on “People” in the top horizontal menu;&lt;/li&gt;
&lt;li&gt;In the report configuration, click on “New Column” pink button;&lt;/li&gt;
&lt;li&gt;Select the column “Visitor Schema &amp;gt; id”;&lt;/li&gt;
&lt;li&gt;Apply the changes (not shown in the image);&lt;/li&gt;
&lt;li&gt;Click on the “Run” green button in the upper right corner of the page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Avl6oCaH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-new-user-users-list-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Avl6oCaH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-new-user-users-list-min.png" alt="" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will get the list of visitors that interacted with your website: click on the ID:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6fM4POzi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-new-user-users-list-configured-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6fM4POzi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-new-user-users-list-configured-min.png" alt="" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And those are the details of the user we have just identified:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cjexC_BP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-new-user-user-details-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cjexC_BP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-new-user-user-details-min.png" alt="" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Woopra is able to merge all previous anonymous events with the user profile once (s)he is identified. This way you can build the full user journey, no matter where it started and no matter how many time it was first an anonymous user that was then identified.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens in MixPanel when a user is identified
&lt;/h3&gt;

&lt;p&gt;In MixPanel only one small thing happens: the random generated &lt;code&gt;distinc_id&lt;/code&gt; is now changed to your custom &lt;code&gt;distinct_id&lt;/code&gt;. Nothing more, nothing less: past events are not linked with the new &lt;code&gt;distinct_id&lt;/code&gt; nor a user profile is created.&lt;/p&gt;

&lt;p&gt;To link anonymous events with the identified user (and so, with his/her new &lt;code&gt;distinct_id&lt;/code&gt;) you need to call &lt;code&gt;mixpanel.push()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To create a user profile, you need to call &lt;code&gt;mixpanel.people.set()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nothing happens automatically in MixPanel.&lt;/p&gt;

&lt;p&gt;So, we need to understand how to link the anonymous events fired by the user before (s)he is identified with the events (s)he fires when (s)he is then identified.&lt;/p&gt;

&lt;h4&gt;
  
  
  Building the full default user’s journey in MixPanel
&lt;/h4&gt;

&lt;p&gt;As mentioned, when we call &lt;code&gt;mixpanel.identify()&lt;/code&gt; the only thing that MixPanel does is to change the &lt;code&gt;distinct_id&lt;/code&gt; from the randomly generated one to the custom one we set.&lt;/p&gt;

&lt;p&gt;This call doesn’t link the past anonymous events (tied to the randomly generated &lt;code&gt;distinct_id&lt;/code&gt;) with the new custom &lt;code&gt;distinct_id&lt;/code&gt; making impossible to get a clear picture of the user journey in the reports.&lt;/p&gt;

&lt;p&gt;More, MixPanel doesn’t even create a User profile where we can read all the User’s Properties we set nor where we can see all the events (s)he performed.&lt;/p&gt;

&lt;p&gt;To solve this problem we need to call &lt;code&gt;mixpanel.alias()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They use a fictional user to explain &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004497803-Identity-Management-Best-Practices#manage-identity-with-alias-and-identify-methods"&gt;how to use &lt;code&gt;mixpanel.alias()&lt;/code&gt; in conjunction with &lt;code&gt;mixpanel.identify()&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Sally comes to your website for the first time.
Mixpanel assigns Sally a randomly generated ID, which is known as a &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004509426"&gt;Mixpanel &lt;code&gt;distinct_id&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Mixpanel assigns Sally the &lt;code&gt;distinct_id&lt;/code&gt; “12345”.
Now all her actions are tied to that &lt;code&gt;distinct_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;After clicking through a few pages, she successfully signs up for an account.&lt;/li&gt;
&lt;li&gt;The signup confirmation page calls the &lt;code&gt;mixpanel.alias()&lt;/code&gt; method and passes Sally’s email address as an argument.
For example, &lt;code&gt;mixpanel.alias(“sally@gmail.com")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;mixpanel.alias(“sally@gmail.com")&lt;/code&gt; method doesn’t change her Mixpanel &lt;code&gt;distinct_id&lt;/code&gt;.
It adds the ID “&lt;a href="mailto:sally@gmail.com"&gt;sally@gmail.com&lt;/a&gt;” to a Mixpanel lookup table and maps it to the original Mixpanel &lt;code&gt;distinct_id&lt;/code&gt; “12345”.&lt;/li&gt;
&lt;li&gt;Now Mixpanel calls the &lt;code&gt;mixpanel.identify("sally@gmail.com")&lt;/code&gt; method and passes the ID &lt;code&gt;sally@gmail.com&lt;/code&gt; to all subsequent pages and logins whenever Mixpanel sees data for “&lt;a href="mailto:sally@gmail.com"&gt;sally@gmail.com&lt;/a&gt;”.&lt;/li&gt;
&lt;li&gt;Mixpanel remaps her original &lt;code&gt;distinct_id&lt;/code&gt; of “12345”.
So all actions Sally takes – whether on your site, in your app, or anonymously before she signed up for her account – maps to her.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Just a note: MixPanel, as you can see, uses the email address: don’t follow their example and use the unique ID you assign on your system to avoid breaks in your data if you make possible for your users to change their email address!&lt;br&gt;&lt;br&gt;
If you want to have the email at hand when consulting reports, add it as a property of the user object.&lt;/p&gt;

&lt;p&gt;So, summing up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The visitor lands on your website or opens your app: (s)he is anonymous and has a randomly generated &lt;code&gt;distinct_id&lt;/code&gt; (“12345”).
(s)he navigates your website or app and fires some events that are tied to the &lt;code&gt;distinct_id&lt;/code&gt; “12345”.&lt;/li&gt;
&lt;li&gt;The visitor signs up: now (s)he is identifiable and you need to assign her (she’s Sally!) a custom &lt;code&gt;distinct_id&lt;/code&gt;, usually the ID your system assigned her.
As in this moment MixPanel still recognizes her by the randomly generated &lt;code&gt;distinct_id&lt;/code&gt; it assigned her (“12345”), you need to tell MixPanel that from now on you want to recognize her also using your custom &lt;code&gt;distinct_id&lt;/code&gt; that is &lt;code&gt;SALLY_CUSTOM_USER_ID&lt;/code&gt;.
So you call the method &lt;code&gt;mixpanel.alias('SALLY_CUSTOM_USER_ID')&lt;/code&gt;.
Now MixPanel knows that if it finds in the events it collects the &lt;code&gt;distinct_id&lt;/code&gt; “12345” or either the &lt;code&gt;distinct_id&lt;/code&gt; &lt;code&gt;SALLY_CUSTOM_USER_ID&lt;/code&gt;, it has to link them together considering them as fired by the same person. This is done internally using a cross-table that links the two &lt;code&gt;distinct_id&lt;/code&gt;s&lt;/li&gt;
&lt;li&gt;Now Sally logs in: you call &lt;code&gt;mixpanel.identify('SALLY_CUSTOM_USER_ID')&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Sally continues to navigate your website or your app and continues to fire events: those events are tied to the &lt;code&gt;distinct_id&lt;/code&gt; &lt;code&gt;SALLY_CUSTOM_USER_ID&lt;/code&gt; but MixPanel knows that previous events tied to the &lt;code&gt;distinct_id&lt;/code&gt; “12345” were fired by the same person that is now identified by the &lt;code&gt;distinct_id SALLY_CUSTOM_USER_ID&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, basically, what the other two tools do automatically, in MixPanel has to be done by ourselves making the implementation a bit more complex.&lt;/p&gt;

&lt;p&gt;Before going to make our hands dirty with the code, we need to know another important thing.&lt;/p&gt;

&lt;p&gt;MixPanel warns about one thing: call &lt;code&gt;mixpanel.alias()&lt;/code&gt; only once during the life of a visitor.&lt;/p&gt;

&lt;p&gt;They say &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004497803-Identity-Management-Best-Practices#avoid-calling-mixpanelalias-on-a-user-more-than-once"&gt;this&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An alias can only point to one Mixpanel distinct_id.&lt;/p&gt;

&lt;p&gt;If you’ve already mapped “&lt;a href="mailto:sally@gmail.com"&gt;sally@gmail.com&lt;/a&gt;” to Mixpanel distinct_id “12345”, any attempt to map &lt;a href="mailto:sally@gmail.com"&gt;sally@gmail.com&lt;/a&gt; to Mixpanel distinct_id 67890 fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This prescription is in practice a big limitation as it makes practically impossible a full tracking of a cross-device journey.&lt;/p&gt;

&lt;p&gt;But as here we are considering the Default User Journey, I will not explain you more, postponing the question to the post about cross-device tracking.&lt;/p&gt;

&lt;p&gt;One last thing: MixPanel requires that the call to the &lt;code&gt;mixpanel.alias()&lt;/code&gt; method is made BEFORE using the new custom &lt;code&gt;distinct_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Coming back to our code, we can do a simple thing: add the call to the &lt;code&gt;mixpanel.alias()&lt;/code&gt; method directly before the call to the &lt;code&gt;mixpanel.identify()&lt;/code&gt; call: in fact, if we call the &lt;code&gt;mixpanel.alias()&lt;/code&gt; method more than once, nothing happens: no errors, no link between &lt;code&gt;distinct_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, also if technically, as developers, we always prefer to have all in order in our code, in this case, we can do an exception and rely on the behavior of MixPanel that doesn’t trigger any error in case of multiple calls to the &lt;code&gt;mixpanel.alias()&lt;/code&gt; method: it simply does nothing when receives consecutive calls.&lt;/p&gt;

&lt;p&gt;So, we need to only modify our Twig template and our code becomes like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% if is_granted('IS_AUTHENTICATED_FULLY') or is_granted('IS_AUTHENTICATED_REMEMBERED') %}
    {% for label, messages in app.flashes(['identify']) %}
        {% for message in messages %}
            &amp;lt;script type="text/javascript"&amp;gt;
                console.log('identifying the user.')
                amplitude.getInstance().setUserId('{{ app.user.id }}');

                // Add this call to the `alias()` method
                mixpanel.alias('{{ app.user.id }}');

                mixpanel.identify('{{ app.user.id }}');
                woopra.identify({id: '{{ app.user.id }}'});
                amplitude.getInstance().logEvent('Login');
                mixpanel.track('Login');
                woopra.track('Login');
            &amp;lt;/script&amp;gt;
        {% endfor %}
    {% endfor %}
{% endif %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to do one last thing: creating the User profile on MixPanel.&lt;/p&gt;

&lt;p&gt;MixPanel &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004501966-People-Profiles"&gt;says that&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A Mixpanel People profile reflects the most recent information about a user.&lt;/p&gt;

&lt;p&gt;Mixpanel builds profiles by connecting information about a user to a distinct_id, which creates a location that collects current information about a user.&lt;/p&gt;

&lt;p&gt;People profiles enable you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analyze how users navigate an application.&lt;/li&gt;
&lt;li&gt;Store and update additional information about your users.&lt;/li&gt;
&lt;li&gt;Use the &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004602143-Messages-and-Campaign-Overview-Video"&gt;Messages and Campaign&lt;/a&gt; feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixpanel People properties contain properties that describe a user and the activity feed that displays events the user performed.&lt;/p&gt;

&lt;p&gt;You can use the &lt;a href="https://help.mixpanel.com/hc/en-us/articles/360000902886-Explore-Your-Users"&gt;Explore report&lt;/a&gt; to aggregate and organize a collection of People profiles.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, to get in MixPanel the same functionalities we get automatically with Amplitude and Woopra, we need to manually create the user profile.&lt;/p&gt;

&lt;p&gt;Doing this is as simple as adding one more call to the &lt;code&gt;mixpanel.pople.set()&lt;/code&gt; method, again, editing only our Twig template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% if is_granted('IS_AUTHENTICATED_FULLY') or is_granted('IS_AUTHENTICATED_REMEMBERED') %}
    {% for label, messages in app.flashes(['identify']) %}
        {% for message in messages %}
            &amp;lt;script type="text/javascript"&amp;gt;
                console.log('identifying the user.')
                amplitude.getInstance().setUserId('{{ app.user.id }}');
                mixpanel.alias('{{ app.user.id }}');
                mixpanel.identify('{{ app.user.id }}');

                // Add this call to the `people.set()` method
                mixpanel.people.set({ id: '{{ app.user.id }}'' })

                woopra.identify({id: '{{ app.user.id }}'});
                amplitude.getInstance().logEvent('Login');
                mixpanel.track('Login');
                woopra.track('Login');
            &amp;lt;/script&amp;gt;
        {% endfor %}
    {% endfor %}
{% endif %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the User’s profile is now created in MixPanel, too:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jQ5htxyT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/mixpanel.user-profile-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jQ5htxyT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/mixpanel.user-profile-min.png" alt="" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, also the anonymous event I fired &lt;code&gt;EmailSoftReclaimRequest&lt;/code&gt; is now tied with my profile (plus some other test events ?).&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the logout
&lt;/h2&gt;

&lt;p&gt;So you now know who is your visitor (now we can also call him/her a User!) and you can tie the events (s)he fires to his/her profile you created in your tracking software.&lt;/p&gt;

&lt;p&gt;One last thing remains to do: handling his/her log out.&lt;/p&gt;

&lt;p&gt;With this paragraph, we start introducing the next relevant topic: handling non-default journeys of your users.&lt;/p&gt;

&lt;p&gt;Are you asking yourself why should you need to handle the logout?&lt;/p&gt;

&lt;p&gt;Because one of the non-default journeys of your users is the use of the same device by different users: if my wife and I use the same device to navigate your site, you will tie our events to one profile while we actually are two different people with completely different behaviours.&lt;/p&gt;

&lt;p&gt;This promiscuity in registering events will lead you to get wrong data, then wrong reports and, in the end, wrong conclusions.&lt;/p&gt;

&lt;p&gt;And, more, when my wife logs out and then I log in, you will have only one profile that in the best case will get updated once with my wife’s data and then with my own data, having it changing continuously.&lt;/p&gt;

&lt;p&gt;So, you need to handle the logout.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to handle the log out with Amplitude
&lt;/h3&gt;

&lt;p&gt;To &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115001361248#logging-out-and-anonymous-users"&gt;handle the logout in Amplitude&lt;/a&gt; you need to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set the User ID to &lt;code&gt;null&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Regenerate the &lt;code&gt;device_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code required to do this is the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amplitude.getInstance().setUserId(null); // not string 'null'
amplitude.getInstance().regenerateDeviceId();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amplitude warns that&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if you choose to do this, then you will not be able to see that the two users were using the same browser/device.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But this is not a big issue as we will anyway have two users using the same kind of browser, the same OS, etc. and this the important thing while understanding precisely that it is exactly the same device is not important (not always important, at least).&lt;/p&gt;

&lt;h3&gt;
  
  
  How to handle the log out with MixPanel
&lt;/h3&gt;

&lt;p&gt;In MixPanel the handling of logout is simple, too, but it has some other &lt;a href="https://mixpanel.com/blog/2015/09/21/community-tip-maintaining-user-identity/"&gt;consequences that are typical of MixPanel&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;you are resetting the Distinct Id and removing all existing super properties.&lt;/p&gt;

&lt;p&gt;While resetting users is useful if you have many users on the same device, it does have some undesirable effects. First, all events for logged out users will appear anonymous, meaning the unique user counts for these events will not be correct. In addition, because you remove super properties, you will need to again register these for each user on login.&lt;/p&gt;

&lt;p&gt;Ultimately the tradeoff for the above drawbacks is that each profile is one unique user within your implementation. We typically only recommend implementing the above if you anticipate this scenario happening as the norm – if multiple users on the same device is not common, implementing logic on logout to handle this scenario may be more trouble than it is worth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What do I think about this? You have to evaluate the tradeoff (yes, the exact same thing MixPanel told us ?).&lt;/p&gt;

&lt;p&gt;In our app TrustBack.Me, we decided to implement the logout handling as we don’t know in advance if more people will use the same device but we evaluated that handling proper user tracking is more important than the effort required (that is, incidentally, not so much anyway).&lt;/p&gt;

&lt;p&gt;So, in this MixPanel is easier to use as it requires only one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mixpanel.reset()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;For JavaScript, calling the reset method will clear the Distinct Id and all super properties, as well as generate a new Distinct Id for the user.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How to handle the log out with Woopra
&lt;/h3&gt;

&lt;p&gt;I was not able to find information about how to handle the logout of users with Woopra.&lt;/p&gt;

&lt;p&gt;So I contacted their support that in less than 24 hours told me to use the method &lt;code&gt;woopra.reset()&lt;/code&gt; and also to set to an empty object the user’s properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;woopra.reset();
woopra.visitorData = {};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Concrete implementation of logout handling
&lt;/h3&gt;

&lt;p&gt;For the login, we used Symfony events to mark the login.&lt;/p&gt;

&lt;p&gt;Unfortunately, Symfony doesn’t fire corresponding events for logout.&lt;/p&gt;

&lt;p&gt;But it permits to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure a page to which redirect the user after a successful logout;&lt;/li&gt;
&lt;li&gt;Use a logout success handler that is simply a service implementing the &lt;a href="https://api.symfony.com/4.0/Symfony/Component/Security/Http/Logout/LogoutSuccessHandlerInterface.html"&gt;&lt;code&gt;LogoutSuccessHandlerInterface&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When first implementing the logout handling, we wanted to use the same approach we used for login, setting another flash message to mark the logout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace App\Subscriber;

/**
 * Subscribed to the event `security.interactive_login` and sets a flash message that markes the login.
 *
 * This way, in Twig, we can check for the existence of this flash message and identify the user in analytics tools.
 */
class AnalyticsSubscriber implements EventSubscriberInterface, LogoutSuccessHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents():array
    {
        ...
    }

    /**
     * Sets the message in the flash bag.
     *
     * @param InteractiveLoginEvent $event
     */
    public function onInteractiveLogin(InteractiveLoginEvent $event):void
    {
        ...
    }

    /**
     * @param Request $request
     *
     * @return RedirectResponse
     */
    public function onLogoutSuccess( Request $request ):RedirectResponse
    {
        /** @var Session $session */
        $session = $request-&amp;gt;getSession();
        $session-&amp;gt;getFlashBag()-&amp;gt;add('identify', 'logout');

        return new RedirectResponse($request-&amp;gt;getSchemeAndHttpHost());
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see we returned a &lt;code&gt;RedirectResponse&lt;/code&gt; object as the interface obliges to return a &lt;code&gt;ResponseInterface&lt;/code&gt; object. So we thought to redirect the User to the homepage after the redirect.&lt;/p&gt;

&lt;p&gt;Unfortunately, the redirect resets also the flash messages and so it is impossible to mark the login.&lt;/p&gt;

&lt;p&gt;So we have to go with the first approach.&lt;/p&gt;

&lt;p&gt;Basically, you need to create a &lt;code&gt;logout.html.twig&lt;/code&gt; template and implement in it the code to reset the tracking.&lt;/p&gt;

&lt;p&gt;So the full Symfony’s configuration requires those steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;code&gt;SecurityController&lt;/code&gt; and inside it a method &lt;code&gt;bye()&lt;/code&gt; that will be the route that will render the &lt;code&gt;logout.html.twig&lt;/code&gt; template:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

declare(strict_types=1);

/*
 * This file is part of the Trust Back Me Www.
 *
 * Copyright Adamo Aerendir Crespi 2012-2017.
 *
 * This code is to consider private and non disclosable to anyone for whatever reason.
 * Every right on this code is reserved.
 *
 * @author Adamo Aerendir Crespi &amp;lt;hello@aerendir.me&amp;gt;
 * @copyright Copyright (C) 2012 - 2017 Aerendir. All rights reserved.
 * @license SECRETED. No distribution, no copy, no derivative, no divulgation or any other activity or action that
 * could disclose this text.
 */

namespace App\Controller;

use FOS\UserBundle\Controller\SecurityController as FOSSecurityController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * {@inheritdoc}
 */
class SecurityController extends FOSSecurityController
{
    /**
     * Shows the logout page.
     *
     * This page is required to reset the tracking cookies.
     *
     * @Route("/bye")
     *
     * @return Response
     */
    public function logout(): Response
    {
        return $this-&amp;gt;render('App/security/logout.html.twig');
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, I render the template directly in the controller without using the &lt;code&gt;@Template&lt;/code&gt; annotation: this is a &lt;a href="https://symfony.com/doc/current/best_practices/controllers.html#template-configuration"&gt;Symfony’s best practice&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the security in the &lt;code&gt;security.yaml&lt;/code&gt; file.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# config/packages/security.yaml

security:
    ...
    firewalls:
        ...
        main:
            ...
            logout:
                target: /bye
            ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Finally, create the &lt;code&gt;logout.html.twig&lt;/code&gt; template
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{# templates/App/security/logout.html.twig #}

{% extends 'App/base.html.twig' %}

{% block title %}Logout{% endblock %}
{% block metaDescription %}Mostra che sei un commerciante affidabile. Ti serve solo un indirizzo e-mail.{% endblock %}

{% block body %}
    &amp;lt;div class="wrapper bg-light-gray"&amp;gt;
        &amp;lt;div class="container"&amp;gt;
            &amp;lt;div class="row"&amp;gt;
                Logout
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script type="text/javascript"&amp;gt;
        console.log('logging out the user.');
        amplitude.getInstance().logEvent('Logout', null, function() {
            amplitude.getInstance().setUserId(null);
            amplitude.getInstance().regenerateDeviceId();
        });
        mixpanel.track('Logout', null, function() {
            mixpanel.reset();
        });
        woopra.track('Logout', null, function() {
            woopra.reset();
            woopra.visitorData = {};
        });
    &amp;lt;/script&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: there is a call to &lt;code&gt;console.log()&lt;/code&gt;: you can remove it once you have seen how does the logout handling work.&lt;/p&gt;

&lt;p&gt;As you can see, I first log the &lt;code&gt;Logout&lt;/code&gt; event, then use a callback to reset the identifiers: this is to be sure the &lt;code&gt;Logout&lt;/code&gt; event is associated with the logging out user. Without using a callback, there is the risk of logging the &lt;code&gt;Logout&lt;/code&gt; event after the reset, linking the event to the new anonymous randomly generated ID assigned after the reset.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens in Amplitude when you handle the logout of users
&lt;/h3&gt;

&lt;p&gt;Click on the “New” blue button and then select “User Look-Up”: you will see the list of events fired on your website.&lt;/p&gt;

&lt;p&gt;As you can see from the screenshot, the user is assigned a random User ID &lt;code&gt;71984668067&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then, once (s)he logs in, the ID is changed to &lt;code&gt;1&lt;/code&gt; that is the ID (s)he has in TrustBack.Me database users table.&lt;/p&gt;

&lt;p&gt;When (s)he logs out, the &lt;code&gt;Logout&lt;/code&gt; event is registered with the ID &lt;code&gt;1&lt;/code&gt;, then (s)he is assigned a new random generated ID &lt;code&gt;71984682059&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This way, when someone else logs in using the same device, (s)he will have his/her own ID and we can track all activities very precisely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GgMWn9OF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-users-logout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GgMWn9OF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/amplitude-users-logout.png" alt="" width="800" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens in MixPanel when you handle the logout of users
&lt;/h3&gt;

&lt;p&gt;If you click on the LiveView in MixPanel, you will see the last events tracked.&lt;/p&gt;

&lt;p&gt;As you can see, there is a user that did the login, viewed some pages and then logged out. After the logout, MixPanel correctly tracks another page view, assigning the event to a different user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dh9vOY98--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/mixpanel-users-logout-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dh9vOY98--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/mixpanel-users-logout-min.png" alt="" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens in Woopra when you handle the logout of users
&lt;/h3&gt;

&lt;p&gt;Woopra registers the two users: one is identified (has the custom ID &lt;code&gt;2&lt;/code&gt;) while the other is anonymous.&lt;/p&gt;

&lt;p&gt;The anonymous user is actually me after the logout, so Woopra successfully resets the ID.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bds_p70o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-users-list-logout-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bds_p70o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-users-list-logout-min.png" alt="" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Woopra I’m observing a strange behaviour: I’m expecting the &lt;code&gt;Logout&lt;/code&gt; event being tied to the identified user.&lt;/p&gt;

&lt;p&gt;Instead, Woopra seems to first reset the user and then log the &lt;code&gt;Logout&lt;/code&gt; event, also if the resetting is done in a callback (and so, the code should be executed only after the tracking of the &lt;code&gt;Logout&lt;/code&gt; event is completed – and tied to still identified user).&lt;/p&gt;

&lt;p&gt;As we will not use Woopra in our applications here at Serendipity HQ, I will not going deeper in this behavior: if you will use Woopra, keep in mind this and, maybe, contact their support: they are very fast and exaustive!&lt;/p&gt;

&lt;p&gt;The two images show what I’m saying.&lt;/p&gt;

&lt;p&gt;In the image below, I expect to see also the Logout event, but it isn’t there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HqoGaMVp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-user-login-logout-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HqoGaMVp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-user-login-logout-min.png" alt="" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead, it is tied with another anonymous user, the one created after the identified user does the logout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SzLfUuSm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-user-logout-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SzLfUuSm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/11/woopra-user-logout-min.png" alt="" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;So we have completed the tracking of the default user’s journey:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Anonymous navigation;&lt;/li&gt;
&lt;li&gt;Registration;&lt;/li&gt;
&lt;li&gt;Login;&lt;/li&gt;
&lt;li&gt;Navigation as a logged-in user;&lt;/li&gt;
&lt;li&gt;Logout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The logout opens the first big issue we will face with tracking: handling multiple users on the same device.&lt;/p&gt;

&lt;p&gt;This is only one of the possible issues we have to handle.&lt;/p&gt;

&lt;p&gt;There are many others: check them out in the next post and learn how to deal with them and how to solve them.&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/behavioural-analytics-tracking-unique-users/"&gt;How to track unique users with behavioural analytics tools&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to track events with behavioural analytics tools</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Fri, 16 Nov 2018 18:49:59 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-track-events-with-behavioural-analytics-tools-48dk</link>
      <guid>https://dev.to/serendipityhq/how-to-track-events-with-behavioural-analytics-tools-48dk</guid>
      <description>&lt;p&gt;The easiest way to start using concretely a behavioural analytics tool is to start tracking some events.&lt;/p&gt;

&lt;p&gt;This way you will see them registered in the behavioural analytics tool of your choice, and you can start better understanding how they work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://io.serendipityhq.com/experience/behavioral-analytics/"&gt;As told, in theory is all simple&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But the problems arise when you have to concretely decide which events to track.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to decide which events to track
&lt;/h2&gt;

&lt;p&gt;In your first attempt at identifying events, maybe you will be influenced by the “Pageview centric vision”.&lt;/p&gt;

&lt;p&gt;This concept will be more clear in a bit.&lt;/p&gt;

&lt;p&gt;For the moment, let’s start by understanding what is an Event in the tracking context.&lt;br&gt;&lt;br&gt;
Then I’ll provide you with a concrete example of a “Pageview centric vision” and how to extract events from it.&lt;/p&gt;

&lt;p&gt;The Amplitude’s Data Taxonomy Playbook explains very well &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115000465251#events"&gt;what an event is and also provides a lot of helpful considerations about them&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;So, what is an event? An event is any distinct action a user can perform in your product (e.g. start game, add to cart) or any activity associated with a user (e.g. in-app notifications, push notifications). When deciding what events to track, it is helpful to think of event categories first.&lt;/p&gt;

&lt;p&gt;Start first by determining what your product’s critical flow is. For a food delivery app it would be order placed, for a healthcare app it would be sessions started/booked. When you have your critical flow you can go through and determine what types of events you’ll need in order to best understand that critical event and the flows surrounding the event (it is not enough just to know how many people placed an order or booked a session, but also what factors were causing them to enter that flow.)&lt;/p&gt;

&lt;p&gt;Identifying broad categories of your events will help you conceptualize the major components of your product. It also will allow you to better organize your events in Amplitude’s UI. Make sure that each category tracked will help towards accomplishing one of your business objectives. Sticking with the business objectives we defined in the above pre-work section, event categories could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Onboarding&lt;/li&gt;
&lt;li&gt;Checkout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you have determined your overarching categories, start to drill down into the individual events in these groupings. Begin with the top priority events that align with your business goals.&lt;/p&gt;

&lt;p&gt;You do not need to track every event in your product as doing so will increase noise in your project and make your taxonomy difficult to understand. We recommend instrumenting no more than 50 events in your first pass. Having fewer events enables you to think about and focus on what is truly important to track in your product. However, you do want to detail out every event in a critical path (e.g. onboarding flows, purchase funnels). Without carefully instrumenting every step, you will not be able to accurately see and analyze drop-offs.&lt;/p&gt;

&lt;p&gt;When deciding on events to track, keep in mind the questions you want to answer and the hypotheses you want to test. While we would not expect you to know 100% of the questions and hypotheses you’ll be testing down the road, try to be as diligent as possible when selecting actions to record in Amplitude.&lt;/p&gt;

&lt;p&gt;Common questions to consider are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are my users successfully completing onboarding?&lt;/li&gt;
&lt;li&gt;How many of my users convert into paying users?&lt;/li&gt;
&lt;li&gt;How many times are my users performing Event A vs. Event B?&lt;/li&gt;
&lt;li&gt;What are my most engaged users doing?&lt;/li&gt;
&lt;li&gt;Which groups of my users are engaging with feature C?&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, there are five main steps to take to figure out what events to track:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Determine the business goals;&lt;/li&gt;
&lt;li&gt;Determine the product’s critical flows;&lt;/li&gt;
&lt;li&gt;Identify broad categories of events;&lt;/li&gt;
&lt;li&gt;Formulate the questions you want an answer to from the tracked events;&lt;/li&gt;
&lt;li&gt;Identify the single events in each category.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The definition of business goals (first point) is an entire topic on its own and cannot be covered here.&lt;/p&gt;

&lt;p&gt;However, as a starting point, you can think about those goals for your business:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Increase onboarding conversion;&lt;/li&gt;
&lt;li&gt;Increase retention of paying users (customers);&lt;/li&gt;
&lt;li&gt;Increase checkout funnel conversions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In defining the goals, you should think about the impact that their improvement has on the bottom line.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Events serve only one purpose: understanding the User Journey&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This concept is &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115000465251-Data-Taxonomy-Playbook#understanding-the-user-journey"&gt;explained at its best once again by Amplitude&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As you start creating your event taxonomy one important thing to think about is the end user journey as they go through your product. Your taxonomy should allow you to split your analysis into 3 different levels&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event counters – Being able to track DAU/MAU, Total purchases etc.&lt;/li&gt;
&lt;li&gt;Funnels and conversions – Retention rates, Funnel conversion, Power curve chart&lt;/li&gt;
&lt;li&gt;Behavioural analysis – Impacts of performing actions on your other metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we think of a new user we can map their journey from new to ultimately to a power user. The key here is to understand the factors that is causing a user to transition between these states and having clear events tracked for these key areas will allow you to better analyze patterns that help/hinder users from transitioning between these states.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  How to concretely determine the events to track filtering them from all the possible actions a User can perform
&lt;/h3&gt;

&lt;p&gt;Now we know in theory how to decide which events to track.&lt;/p&gt;

&lt;p&gt;When coming to concrete things, however, we face a concrete problem.&lt;/p&gt;

&lt;p&gt;Let’s explain this problem with an example.&lt;/p&gt;

&lt;p&gt;When deciding what to track on &lt;a href="https://TrustBack.Me"&gt;TrustBack.Me&lt;/a&gt;, we decided to start simple and start tracking the Email Soft Reclaim.&lt;/p&gt;

&lt;p&gt;The Email Soft Reclaim Journey permits any user to provide their email address to see if TrustBack.Me has any orders associated with it.&lt;/p&gt;

&lt;p&gt;Practically, it permits to check the orders associated with any email without requiring a registration (so, no password required!).&lt;/p&gt;

&lt;p&gt;When we tried to slice the journey, we found there are those actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user lands on the starting page of the Email Soft Reclaim Journey;&lt;/li&gt;
&lt;li&gt;The user fills the form with his/her email;&lt;/li&gt;
&lt;li&gt;The user clicks on the button to submit the form;&lt;/li&gt;
&lt;li&gt;The form redirects to a confirmation page that explains that an email was sent to the provided address;&lt;/li&gt;
&lt;li&gt;TrustBack.Me sends an email with the access link to the provided email address;&lt;/li&gt;
&lt;li&gt;The user clicks on the access link;&lt;/li&gt;
&lt;li&gt;The user lands on the access page and sees the orders associated with the email (if any);&lt;/li&gt;
&lt;li&gt;The user continues taking other actions (see the details of an order, a feedback (s)he released and so on).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, given this sequence of actions, which are the relevant events that they fire and that we needed to track?&lt;/p&gt;

&lt;p&gt;So, at this point, we started to formulate the questions to which we want an answer from the tracked events.&lt;/p&gt;

&lt;p&gt;The questions we formulated were these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How many Email Soft Reclaim requests are submitted in a defined period of time?&lt;/li&gt;
&lt;li&gt;How many Users, after having submitted the request, then effectively see the list of orders?&lt;/li&gt;
&lt;li&gt;What do they do next?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three simple questions to which we can answer properly tracking our events.&lt;/p&gt;

&lt;p&gt;So, the possible events we can track are those:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;View of starting page of Soft Email Request;&lt;/li&gt;
&lt;li&gt;Click on the submit button;&lt;/li&gt;
&lt;li&gt;View of confirmation page;&lt;/li&gt;
&lt;li&gt;Click on the access link;&lt;/li&gt;
&lt;li&gt;View of the access page with the list of orders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Five actions and each one of them can be considered an event that can be tracked.&lt;/p&gt;

&lt;p&gt;Now, let’s consider the usefulness of each of those events (spoiler: I’m starting to show you what does it mean “Pageview centric vision”).&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;1. View of starting page of Soft Email Request.&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Do we really need to track this? Maybe we don’t. At least not in this first tracking instrumentation.&lt;/p&gt;

&lt;p&gt;In fact, tracking this will for sure tells us how many people viewed the page and, combining this information with the last event (view of access page with the list of orders), permits us to understand the ratio between the requests and the accesses.&lt;/p&gt;

&lt;p&gt;But this is not a relevant information, also if at a first look it seems it is.&lt;/p&gt;

&lt;p&gt;In fact, this is not currently a business goal nor is relevant to know the drop off between the starting page views and the concrete access: the access to the initial page, in fact, can happen for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For curiosity, just to see what the page contains;&lt;/li&gt;
&lt;li&gt;For necessity, because the user really wants to see the list of his/her orders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We need to track the performance only for the second group of people, not the first.&lt;/p&gt;

&lt;p&gt;So, as we have other actions after this, we can use them as the starting point of our tracking.&lt;/p&gt;

&lt;p&gt;We decided that this event is not relevant and we don’t track it.&lt;/p&gt;
&lt;h4&gt;
  
  
  2. Click on the submit button
&lt;/h4&gt;

&lt;p&gt;This event is more close to what we want to understand from our tracking: how many people start the Email Soft Reclaim (and then, how many of the started Soft Reclaims lead to a concrete access to the list of orders).&lt;/p&gt;

&lt;p&gt;Maybe we should track this.&lt;/p&gt;

&lt;p&gt;Before deciding, let’s understand better the next action.&lt;/p&gt;
&lt;h4&gt;
  
  
  3. View of confirmation page
&lt;/h4&gt;

&lt;p&gt;This action, if tracked as an event, basically offers the same information of the previous action. In fact, it doesn’t show an error if we don’t know the email (for privacy reasons: telling if we have or not the email may disclose privacy information). Tracking errors is a good thing to do, but, as told, this is not the case.&lt;/p&gt;

&lt;p&gt;So, we need to track only one between the “Click on the submit button” action and this: tracking both is a duplication that only causes messiness in the data collected.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE: If this page had also shown an error in case the email was not present in our database, then, this would have been an event to track, but not mandatory.&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  4. Click on the access link
&lt;/h4&gt;

&lt;p&gt;This is not really an event: more probably here we should track the UTM params, setting them in a way that makes us able to recognize a user as coming from an Email Soft Reclaim access link.&lt;/p&gt;

&lt;p&gt;But this is a topic we will cover later in another post.&lt;/p&gt;
&lt;h4&gt;
  
  
  5. View of the access page with the list of orders
&lt;/h4&gt;

&lt;p&gt;This is the last action in the journey, so we need to track it for sure as we need it to find the end number of people who arrive at the end.&lt;/p&gt;
&lt;h4&gt;
  
  
  So, how many events should we track and what are them?
&lt;/h4&gt;

&lt;p&gt;So, in the end, we need to track those events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on the submit button;&lt;/li&gt;
&lt;li&gt;View of the access page with the list of orders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All other steps are not relevant: we only need to add also the UTM query parameters to the access links in the emails we send (again, this topic will be explained deeply in another blog post).&lt;/p&gt;
&lt;h3&gt;
  
  
  Event properties
&lt;/h3&gt;

&lt;p&gt;Now that we have our events defined, we need to understand which information we want to record each time one of them happen.&lt;/p&gt;

&lt;p&gt;That information is stored in the properties of the event.&lt;/p&gt;

&lt;p&gt;The properties provide additional information that can then be used to better analyze data.&lt;/p&gt;

&lt;p&gt;Woopra documentation explains very well &lt;a href="https://docs.woopra.com/docs/understanding-data-in-woopra#section-action-properties"&gt;the usefulness of events’ properties&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[…] wouldn’t it be helpful to know a little more about each action the customers take? In practice, when a visitor performs the action “pageview,” users can find out what page was viewed, how long the customers there, what device was used, and so on.&lt;/p&gt;

&lt;p&gt;This is where action properties come in to play. Think of action properties as specific attributes that provide additional details for each event you’re tracking.&lt;/p&gt;

&lt;p&gt;For example, users may want to track every time a customer makes a payment, one could send the “Payment” action to Woopra, along with associated properties such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment amount&lt;/li&gt;
&lt;li&gt;user plan or package&lt;/li&gt;
&lt;li&gt;payment option (e.g. credit card, Paypal, ACH), and so on.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mixpanel offers another &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004708186-Event-Properties-Super-Properties-People-Properties#event-properties"&gt;useful point of view about Events’ properties&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Event Properties are bits of extra information that you send along with your Events which describe the details of that action. They are usually specific to the Event they’re describing and don’t apply universally to other Events. Leveraging Event Properties allows you to conduct deeper analysis to better understand user behavior for a specific action.&lt;/p&gt;

&lt;p&gt;While Event Properties generally describe the Event itself, they can also describe a user’s actions in relation to that specific Event. For example, for a Video Played Event, you may want to include a Property that lets you know if the user firing that Event finished watching the entire video or played only part of the video.&lt;/p&gt;

&lt;p&gt;Other examples of Events and Event Properties you might send with that Event:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event:&lt;/strong&gt;  Page View;  &lt;strong&gt;Property&lt;/strong&gt; : Name of page viewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event&lt;/strong&gt; : Video Played;  &lt;strong&gt;Properties&lt;/strong&gt; : Length, Artist, Genre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event&lt;/strong&gt; : Made Purchase;  &lt;strong&gt;Properties&lt;/strong&gt; : Item(s) purchased, Total, Did the user have a coupon or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event&lt;/strong&gt; : Leveled Up;  &lt;strong&gt;Properties&lt;/strong&gt; : Level, Time to level up, Hours user played before this level up.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  The difference between the Event Properties and the User Properties
&lt;/h4&gt;

&lt;p&gt;In defining the properties of the events you want to track, you need to take into consideration also the difference between the properties of the events and the properties of the users you track.&lt;/p&gt;

&lt;p&gt;Amplitude &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115000465251#what-properties-should-i-track"&gt;explains this difference very well&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The most general explanation of the difference between the two is that user properties are attached to users and reflect the current state of the user at the time of the event while event properties are attached to events and reflect the state of the event that was triggered.&lt;/p&gt;

&lt;p&gt;Event and user properties can be sent along with every event to Amplitude. All of these values will be tracked with the specific event they were sent with and the user properties sent will also update the user’s user properties with the most recent values. Another way to think about user properties and event properties is that user properties apply to the user for all events going forward until they are changed, and event properties are applied only to events at the time they are sent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, there is no complexity in understanding what Event Properties are.&lt;/p&gt;

&lt;p&gt;The complexities, as usual, arise when we have to decide concretely which properties to associate with our events.&lt;/p&gt;

&lt;p&gt;At this point, it is possible that a lot of possible properties are coming to our mind.&lt;/p&gt;

&lt;p&gt;But we are missing another important piece of information.&lt;/p&gt;
&lt;h4&gt;
  
  
  Tracking the relation between journeys using the Event Properties
&lt;/h4&gt;

&lt;p&gt;Introducing you to behavioural analytics tools, I told you to &lt;a href="https://io.serendipityhq.com/experience/behavioral-analytics/"&gt;think also about the relation between journeys&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Practically, each journey can lead to another or to more than one another journey.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;How do we track these relations?&lt;/p&gt;

&lt;p&gt;We need some links between them and those links are simply some IDs, exactly as what we do when creating the relations between our entities in the database.&lt;/p&gt;

&lt;p&gt;Once more, Amplitude offers &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115000465251#event-properties"&gt;a great explanation of this concept&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[…] So, for this e-commerce example, we recommend having two custom event property values, ‘Property ID’ (&lt;em&gt;‘Product ID’ I think they mean, editor’s note&lt;/em&gt;) and ‘Cart ID’. Then, use two funnels to track if a user has viewed and purchased the same product. Two examples are outlined below.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Adding Product to Cart Funnel&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 1: ‘Product – Page Viewed’&lt;/li&gt;
&lt;li&gt;‘Property ID’ = ‘3345’&lt;/li&gt;
&lt;li&gt;Step 2: ‘Product – Quantity Selected’&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘Property ID’ = ‘3345’&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘Quantity’ = ‘1’&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Step 3: ‘Product – Added to Cart’&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘Property ID’ = ‘3345’&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘Quantity’ = ‘1’&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘Cart ID’ = ‘1299’&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Purchasing Cart Funnel&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 1: ‘Cart – Viewed’&lt;/li&gt;
&lt;li&gt;‘Cart ID’ = ‘1299’&lt;/li&gt;
&lt;li&gt;Step 2: ‘Cart – Start Checkout’&lt;/li&gt;
&lt;li&gt;‘Cart ID’ = ‘1299’&lt;/li&gt;
&lt;li&gt;Step 3: ‘Checkout – Address Added’&lt;/li&gt;
&lt;li&gt;‘Cart ID’ = ‘1299’&lt;/li&gt;
&lt;li&gt;Step 4: ‘Checkout – Shipping Method Selected’&lt;/li&gt;
&lt;li&gt;‘Cart ID’ = ‘1299’&lt;/li&gt;
&lt;li&gt;Step 5: ‘Checkout – Payment Added’&lt;/li&gt;
&lt;li&gt;‘Cart ID’ = ‘1299’&lt;/li&gt;
&lt;li&gt;Step 6: ‘Checkout – Complete’&lt;/li&gt;
&lt;li&gt;‘Cart ID’ = ‘1299’&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, using two IDs, &lt;code&gt;PropertyID&lt;/code&gt; (again, I think this should be &lt;code&gt;ProductID&lt;/code&gt;, instead) and &lt;code&gt;CartID&lt;/code&gt;, we can understand if we can really convert a User from a product page to the checkout.&lt;/p&gt;
&lt;h4&gt;
  
  
  The concept of “Super Property”
&lt;/h4&gt;

&lt;p&gt;The final concept we need is the one of Super Property.&lt;/p&gt;

&lt;p&gt;The concept of Super Property is really of MixPanel, that has in place a system to make it easier to track them, but it can be helpful with every other tool you use, also if you have to track them manually.&lt;/p&gt;

&lt;p&gt;So, as it is of MixPanel, &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004708186-Event-Properties-Super-Properties-People-Properties#super-properties"&gt;let its documentation tell us about it&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Super Properties are a type of Event Property that you can set once to automatically attach to every Event you’re tracking. They are Event Properties that apply to all Events without having to manually include them in each &lt;code&gt;mixpanel.track()&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;Because Super Properties send values associated with Events, they are useful for seeing Properties over time. For example, you could set a Super Property with all of your Events that indicates whether they were done by a free or a paid user. Then, if a user changes from free to paid, you can see over time which Events led up to that conversion and which specific Events they did as each type of user.&lt;/p&gt;

&lt;p&gt;Other examples of Super Properties you might send with every Event:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organization that a user belongs to or rolls up into&lt;/li&gt;
&lt;li&gt;Account age or signup date&lt;/li&gt;
&lt;li&gt;Referring user ID or invited date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because Super Properties are stored in the user’s cookie or in local storage, you’ll want to be relatively discerning about what you set as a Super Property so that site or app performance is not compromised.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Apart from the technical and privacy considerations, the concept is really useful if applied to our tracking of events and we should consider it.&lt;/p&gt;
&lt;h4&gt;
  
  
  Deciding which properties to add to tracked events
&lt;/h4&gt;

&lt;p&gt;So, now you should have a more clear vision of Events and of their properties.&lt;/p&gt;

&lt;p&gt;But, as told, the things become to get messy when we try to apply concretely the abstract concepts.&lt;/p&gt;

&lt;p&gt;So, let’s continue with our example of TrustBack.Me and its Email Soft Reclaim.&lt;/p&gt;

&lt;p&gt;As told, we want to track two events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on the submit button;&lt;/li&gt;
&lt;li&gt;View of the access page with the list of orders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s start with the first: Click on the submit button.&lt;/p&gt;
&lt;h5&gt;
  
  
  Defining the properties of the Click on the submit button event
&lt;/h5&gt;

&lt;p&gt;Event name: &lt;code&gt;EmailSoftReclaimRequest&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The email is the minimum required to track the event.&lt;/p&gt;

&lt;p&gt;But going deeper, we can find other interesting properties.&lt;/p&gt;

&lt;p&gt;For example, we may like to know which kind of user requested to access the email: is a merchant or a simple customer? Or is an anonymous user?&lt;/p&gt;

&lt;p&gt;We cannot decide now which other properties we want to track as we still don’t know anything about how to track users other than events: this is a key distinction and we need to understand it well to have a complete tracking setup.&lt;/p&gt;

&lt;p&gt;Understanding how to track users is really more complex than what it might appear at a first look.&lt;br&gt;&lt;br&gt;
I will explain the tracking of users in detail in another post.&lt;/p&gt;

&lt;p&gt;For the moment, let’s focus on the tracking of events.&lt;/p&gt;

&lt;p&gt;This is the code we used to track the event in all the three tools, Amplitude, Mixanel and Woopra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
&amp;lt;div class="submit"&amp;gt;
    &amp;lt;script type="text/javascript"&amp;gt;
        const eventName = 'EmailSoftReclaimRequest';
        const eventProps = {
            "email": document.getElementById("email_reclaim_email").value,
        };
        const track = function() {
            // Mixpanel
            mixpanel.track(eventName, eventProps);
            // END Mixpanel
            // Woopra
            woopra.track(eventName, eventProps);
            // END Woopra
            // Amplitude
            amplitude.getInstance().logEvent(eventName, eventProps);
            // END Amplitude
        };
    &amp;lt;/script&amp;gt;
    &amp;lt;button id="Start" class="btn btn-lg btn-block btn-secure" onclick="track();"&amp;gt;{% trans from 'email_reclaim' %}email.reclaim.soft.form.submit.label{% endtrans %}&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that I created a function called &lt;code&gt;track()&lt;/code&gt; that is invoked when the button is clicked: this way we can send the event to all the three tracking systems (yes: currently we are using them together to understand which best fits our needs).&lt;/p&gt;

&lt;h5&gt;
  
  
  Defining the properties of the “View of the access page with the list of orders”
&lt;/h5&gt;

&lt;p&gt;Event name: &lt;code&gt;EmailSoftReclaimAccessed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also for this event, the email is the minimum required to make the event useful.&lt;/p&gt;

&lt;p&gt;This is the code we use to track the event in all the three tools, Amplitude, Mixanel and Woopra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
&amp;lt;script type="text/javascript"&amp;gt;
    const eventName = 'EmailSoftReclaimAccessed';
    const eventProps = {
        'email': '{{ reclaim.email.email.toString }}',
    };
    // Mixpanel
    mixpanel.track(eventName, eventProps);
    // END Mixpanel
    // Woopra
    woopra.track(eventName, eventProps);
    // END Woopra
    // Amplitude
    amplitude.getInstance().logEvent(eventName, eventProps);
    // END Amplitude
&amp;lt;/script&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that here I don’t use a function as this code is outside of a form and isn’t called on the click of any button: the code is simply executed when the page is loaded.&lt;/p&gt;

&lt;p&gt;Now that we have our two events tracked, it is time to build the reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the funnel/journey reports from the two events
&lt;/h2&gt;

&lt;p&gt;As told before, we want to answer the following questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How many Email Soft Reclaim requests are submitted in a defined period of time?&lt;/li&gt;
&lt;li&gt;How many Users, after having submitted the request, then effectively see the list of orders?&lt;/li&gt;
&lt;li&gt;What do they do next?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We are not able to answer the third question right now as we don’t have instrumented the events. But the first two questions can find an answer right now.&lt;/p&gt;

&lt;p&gt;We only need to build the reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the Email Soft Reclaim Report in Amplitude
&lt;/h3&gt;

&lt;p&gt;This is the list of the tracked events in Amplitude:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TRGK2V6k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-events-list-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TRGK2V6k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-events-list-min.png" alt="" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, we have both &lt;code&gt;EmailSoftReclaimRequest&lt;/code&gt; and &lt;code&gt;EmailSoftReclaimAccessed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What we need to do now is to put them on the same report.&lt;/p&gt;

&lt;p&gt;So, start by clicking on the “New” button on the upper left corner of the Amplitude dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--W7TIkRvR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--W7TIkRvR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-min.png" alt="" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the new screen, select Chart &amp;gt; Funnel&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5ZCABviR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-chart-funnel-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5ZCABviR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-chart-funnel-min.png" alt="" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the new screen you can configure the funnel selecting the events you want to include in the analysis:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c2P86v_N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-chart-funnel-select-events-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c2P86v_N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-chart-funnel-select-events-min.png" alt="" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Amplitude will do the rest as you can see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gtU_Dluk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-chart-funnel-created-min-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gtU_Dluk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-amplitude-new-chart-funnel-created-min-1.png" alt="" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like the result, click on the “Save” button in the upper right corner of the page to save the report.&lt;/p&gt;

&lt;p&gt;For more information about funnel reporting in Amplitude &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/229951267"&gt;click here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the Email Soft Reclaim Report in MixPanel
&lt;/h3&gt;

&lt;p&gt;This is the list of the tracked events in MixPanel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wHnETw-C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-events-list-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wHnETw-C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-events-list-min.png" alt="" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, in this case, too, we have both &lt;code&gt;EmailSoftReclaimRequest&lt;/code&gt; and &lt;code&gt;EmailSoftReclaimAccessed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We now need to build a funnel report also in MixPanel&lt;/p&gt;

&lt;p&gt;So, start by clicking on the “Funnel” tab on the horizontal menu and then click on “Build a funnel” button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9ERRlQUF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-funnels-build-a-funnel-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9ERRlQUF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-funnels-build-a-funnel-min.png" alt="" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can configure the funnel adding the relevant events:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YfF75pkj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-funnels-build-a-funnel-select-events-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YfF75pkj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-funnels-build-a-funnel-select-events-min.png" alt="" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the “Save” button and MixPanel will show you the funnel and the dropoffs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VyS34ksI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-funnel-created-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VyS34ksI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-mixpanel-funnel-created-min.png" alt="" width="800" height="687"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For more information about funnels in MixPanel &lt;a href="https://help.mixpanel.com/hc/en-us/articles/115004561926-Funnels-Deep-Dive"&gt;click here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the Email Soft Reclaim Report in Woopra
&lt;/h3&gt;

&lt;p&gt;When you go to Woopra dashboard of your project, it doesn’t show any list of events: in Woopra, in fact, you don’t have a list of events. To see the details of each event, you need to go to the People report, then click on a visitor and you can see the list of all events fired by that person. Then, clicking on one event, you can see its details.&lt;/p&gt;

&lt;p&gt;So, instead of showing a list of events, Woopra shows them all aggregated on the same chart:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9LjVDjc7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-dashboard-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9LjVDjc7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-dashboard-min.png" alt="" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Woopra calls a funnel report “Journey”, yes, the term we used early: we took it really from Woopra!&lt;/p&gt;

&lt;p&gt;So, to create a funnel report in Woopra, you need to select the “Analyze” item in the horizontal top menu, then click “Journeys” in the “Report types” section of the left menu and in the page that loads, click finally on the “New report” button in the upper right corner:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UsjkYvPY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UsjkYvPY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-min.png" alt="" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the modal window that appears, select the first option “Journeys”:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iSB4ibiG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-select-journey-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iSB4ibiG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-select-journey-min.png" alt="" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are now ready to configure our funnel (Journey).&lt;/p&gt;

&lt;p&gt;Add the relevant events to the journey and for each of them set also their name (yes: you need to write it manually -.-‘ – the price of flexibility?):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JDqVVzwd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-select-events-EmailSoftReclaimRequest-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JDqVVzwd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-select-events-EmailSoftReclaimRequest-min.png" alt="" width="800" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XeJ4t_wq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-select-events-EmailSoftReclaimAccessed-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XeJ4t_wq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-select-events-EmailSoftReclaimAccessed-min.png" alt="" width="800" height="789"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click on the green button “Run” in the upper right corner of the page and the journey is built:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TiG3BQhe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-created-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TiG3BQhe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/EmailSoftReclaim-woopra-new-journey-created-min.png" alt="" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, when I started to track events in Woopra, I never accessed the page that lists the orders associated with the email I provided.&lt;/p&gt;

&lt;p&gt;If you like to know more about journeys (funnels) in Woopra &lt;a href="https://docs.woopra.com/docs/journeys"&gt;click here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced funnels
&lt;/h3&gt;

&lt;p&gt;A final note about Amplitude’s Funnels and Woopra’s Journeys: as I know that we as humans are visual people, I don’t want to leave you with the funnel I built that is really simplistic and ugly to see.&lt;/p&gt;

&lt;p&gt;So, here there are more complex funnels built by the &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115001351507-How-to-Take-Your-Funnel-Analysis-to-the-Next-Level"&gt;Amplitude’s&lt;/a&gt; and &lt;a href="https://docs.woopra.com/docs/journeys"&gt;Woopra’s&lt;/a&gt; teams that better shows their functionalities:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OVxCllK_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/Amplitude-funnels-example-min-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OVxCllK_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/Amplitude-funnels-example-min-min.png" alt="" width="800" height="989"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3VmWc4K_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/Woopra-Journeys-example-min.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3VmWc4K_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/Woopra-Journeys-example-min.png" alt="" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Much prettier &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NOTE: I wasn’t able to find a more advanced example from MixPanel. When I’ll find it, I will update the post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Practices for events tracking
&lt;/h3&gt;

&lt;p&gt;Finally, here there are some &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115000465251-Data-Taxonomy-Playbook#best-practices"&gt;best practices from Amplitude about events&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Only instrument properties you think you will use. Having too many properties makes analysis more difficult.&lt;/li&gt;
&lt;li&gt;Before you include sensitive information (email, addresses, etc.) consider if it goes against the privacy policies for your product.&lt;/li&gt;
&lt;li&gt;Enable the &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/215131888-Web-Attribution"&gt;UTM and referrer user properties&lt;/a&gt; to better understand where you users come from on the web.&lt;/li&gt;
&lt;li&gt;Instrument key properties on every event in a critical path.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Others may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Be consistent with names of events: don’t mix “CamelCased” and “snake_cased” names;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Woopra, instead, gives us some ideas about &lt;a href="https://docs.woopra.com/docs/saas-tracking"&gt;what events to track in a saas application.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Our journey in implementing a behavioural analytics tool to track our SAAS product is still at the beginning.&lt;/p&gt;

&lt;p&gt;A lot of things still remain to understand and to implement.&lt;/p&gt;

&lt;p&gt;But now we at least had seen some basic reports and started to grasp the surface of behavioural analytics.&lt;/p&gt;

&lt;p&gt;I’d like to point out some important things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you start implementing the tracking, focus only on a few properties and events: don’t over-track as this will make the implementation longer and the main mantra in the startup world is “Think big. Start small. Scale fast”. To follow it, you need a few events and a few properties;&lt;/li&gt;
&lt;li&gt;Keep particular attention to the PII you collect in your events: in the examples I provided I used the email. The email is a PII and its use in the tracking of events has really important consequences for the purposes of privacy compliance (in other words, compliance with the GDPR). Be careful!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/behavioural-analytics-tracking-events/"&gt;How to track events with behavioural analytics tools&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to start using behavioural analytics tools to track your SAAS product</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Fri, 16 Nov 2018 18:01:33 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-start-using-behavioural-analytics-tools-to-track-your-saas-product-90b</link>
      <guid>https://dev.to/serendipityhq/how-to-start-using-behavioural-analytics-tools-to-track-your-saas-product-90b</guid>
      <description>&lt;p&gt;When it comes to tracking the numbers of your SAAS product, Google Analytics is not sufficient anymore. This is the hard truth, believe it or not.&lt;/p&gt;

&lt;p&gt;To go one step further, you need to properly implement a behavioural tracking analytics tool and think about your SAAS product differently.&lt;/p&gt;

&lt;p&gt;Integrating behavioural tracking thoughts into the ones about your SAAS product can be tricky. Really tricky.&lt;/p&gt;

&lt;p&gt;If in theory, the things are really simple, when you arrive at the point of concretely define what to track you may become really confused.&lt;/p&gt;

&lt;p&gt;I’m going to explain to you exactly what changes in your thoughts about your SAAS application when you think at behavioural tracking and how to plan and implement it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The differences between Google Analytics and behavioural analytics tools
&lt;/h2&gt;

&lt;p&gt;Google Analytics is really a great product.&lt;/p&gt;

&lt;p&gt;With it, you can measure a lot of things and get a lot of actionable data.&lt;/p&gt;

&lt;p&gt;However, Google Analytics is not the best tool to track the numbers of your SAAS product (or of your e-commerce) as it has some limitations.&lt;/p&gt;

&lt;p&gt;This doesn’t mean that you have to remove it completely.&lt;/p&gt;

&lt;p&gt;This only means that you need to pair it with another tool, with different features.&lt;/p&gt;

&lt;p&gt;Yes, I’m speaking about behavioural analytics apps.&lt;/p&gt;

&lt;p&gt;Here are some of the main differences between Google Analytics and the behavioural analytics tools you can use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retroactivity
&lt;/h3&gt;

&lt;p&gt;When configuring Google Analytics, you need to already have in mind the numbers you need.&lt;/p&gt;

&lt;p&gt;If at a certain point you need to measure something new or extract some other numbers, this can get really complex and the worst thing is that &lt;a href="https://smallbusiness.chron.com/google-analytics-goals-retroactive-63291.html"&gt;they may not be retroactive&lt;/a&gt;: this means that if you need to measure a goal you didn’t set early on, you simply have unusable data.&lt;/p&gt;

&lt;p&gt;Also if concretely the data are in the Google Analytics servers, you cannot interpret them under a new light because Google Analytics doesn’t allow you to do it.&lt;/p&gt;

&lt;p&gt;If you are running an e-commerce company, this may not be a big problem: after all, the metrics of an e-commerce are all really similar and well known in advance: in this case, you can properly configure Google Analytics beforehand and have all the data you need from the start.&lt;/p&gt;

&lt;p&gt;But if you have to measure a SAAS application, then the things become really more heterogeneous and the metrics are not always predictable in advance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flexibility
&lt;/h3&gt;

&lt;p&gt;Really linked to the previous point is this second one.&lt;/p&gt;

&lt;p&gt;Google Analytics doesn’t make your life easy when you need to extract and link data in new ways at which you didn’t think the first time you configured the tracking.&lt;/p&gt;

&lt;p&gt;In some cases, get additional data in an understandable format requires you to create new views or do tricky “hacks” to get the data you need or extract the evidence you need.&lt;/p&gt;

&lt;p&gt;A behavioural tracking app removes this complexity making you able to elaborate and combine data the way you want, the way you need!&lt;/p&gt;

&lt;h3&gt;
  
  
  Point of view
&lt;/h3&gt;

&lt;p&gt;This is maybe the best difference between Google Analytics and behavioural tracking tools.&lt;/p&gt;

&lt;p&gt;Google Analytics is focused on page views while the behavioural tracking tools are focused on customers and the things they do WITH your application and IN your application.&lt;/p&gt;

&lt;p&gt;Google Analytics measures pages first, and then, incidentally, users and their behaviours.&lt;/p&gt;

&lt;p&gt;Behavioural analytics tools, instead, measure first customers, and eventually, if you like, also page views.&lt;/p&gt;

&lt;p&gt;This is reflected also in the data you can get from them: while in Google Analytics, for example, you can get the keywords searched, this data is never present in a behavioural analytics tool (almost never as they anyway can track UTM params).&lt;/p&gt;

&lt;p&gt;This kind of tools answers other, more relevant questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do my customers use my product?&lt;/li&gt;
&lt;li&gt;Why do they do one thing instead of another?&lt;/li&gt;
&lt;li&gt;What my best customers did before to become customers?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google Analytics can answer to these questions, too, but only incidentally and anyway always partially, and not at the level that that kind of software provides.&lt;/p&gt;

&lt;p&gt;This is an important switch in perspective: the main &lt;em&gt;mantra&lt;/em&gt; in the startup world (and in the e-commerce world, too) is “Customers first”.&lt;/p&gt;

&lt;p&gt;Is for that reason that methodologies like “customer development” become so popular (and useful!).&lt;/p&gt;

&lt;p&gt;To put it at its full power, you need to measure properly the metrics that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The issues of Google Analytics that harm you
&lt;/h2&gt;

&lt;p&gt;You cannot collect PII.&lt;/p&gt;

&lt;p&gt;PII stands for Personal Identifiable Information.&lt;/p&gt;

&lt;p&gt;Google explicitly prohibits to send personal data of visitors to Google Analytics. &lt;a href="https://support.google.com/analytics/answer/6366371?hl=en"&gt;Collecting those information goes against the Terms of Use of Google Analytics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main purpose of a behavioural analytics tool, instead, is exactly the opposite: identify the visitor as a person, so it is possible to link his/her journey as an anonymous visitor to his/her journey as a registered user.&lt;/p&gt;

&lt;p&gt;This opens a new world of analytics possibilities as you are able to really understand what your users want and do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going deeper into the differences with Google Analytics: the solved challenges
&lt;/h2&gt;

&lt;p&gt;So, to be more clear, let’s better understand which are the challenges that behavioural analytics tools solve.&lt;/p&gt;

&lt;p&gt;Woopra &lt;a href="https://docs.woopra.com/docs/challenges-the-woopra-platform-solves"&gt;explains them very well&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  1) Non-linear Customer Journeys and Challenges in Data Democratization
&lt;/h3&gt;

&lt;p&gt;The modern customer journey is highly complex and non-linear in nature. To understand the customer experience across channels, teams and tools, you must expand beyond the silos of strictly marketing or product related touchpoints.&lt;/p&gt;

&lt;p&gt;Today’s businesses need to derive insights from across the organization, leveraging marketing, paid and unpaid advertising, customer success, product, support, sales and other groups within the company.&lt;/p&gt;

&lt;p&gt;Most businesses find it challenging to democratize data across the organization and obtain a 360-degree view of their customers to drive end-to-end business objectives.&lt;/p&gt;
&lt;h3&gt;
  
  
  2) Limited Understanding of Product Engagement
&lt;/h3&gt;

&lt;p&gt;It’s crucial to know how prospective users engage with your product or service in order to identify leads at the right time and on the right channel. This is especially true for SAAS companies that offer a ‘freemium’ model when acquiring new customers. Often, they spend an unreasonable amount of time and resources on lead acquisition by sending repeated communications to prospects on an action such as a free trial sign up form.&lt;/p&gt;

&lt;p&gt;Unfortunately, given the vast array of options available out there, such actions are no more telling of lead’s propensity to purchase. What’s worse is that unwanted emails or calls can do more harm than good by deterring them from your product or service altogether.&lt;/p&gt;

&lt;p&gt;For a company focused on the customer experience, actions such as repeated use of certain features, downloading how-to tutorials and viewing software setup videos are much more indicative of interested prospects. However, there are few tools that track behavior across a product or service while enabling teams to engage with their users in real-time.&lt;/p&gt;
&lt;h3&gt;
  
  
  3) Functional Data Siloes
&lt;/h3&gt;

&lt;p&gt;With an estimated 66 different SaaS applications that are in use per enterprise – primarily for web and social traffic based analytics activity – companies are unable to properly leverage the data flowing in their organization to generate meaningful and timely insights.&lt;/p&gt;

&lt;p&gt;Moreover, the market is crowded with solutions promising results such as higher conversion rates that are generating further confusion about what solution to implement, at what cost, and at what time. Such a conundrum gives rise to data siloes that often fail to render timely insights to business users. Most importantly, companies often fail to realize how difficult it is to bring all the customer-centric data together in order to find a single source of truth.&lt;/p&gt;

&lt;p&gt;Given the fragmentation of investments in tools across all sizes of enterprises, a unifying customer journey analytics solution integrates with all the relevant applications to provide a singular view of each customer and better analyze their interaction and needs across all channels. Companies need to anticipate their customers’ behaviors and shift how they think about and engage throughout the decision journey.&lt;/p&gt;
&lt;h3&gt;
  
  
  4) Inability to Monetize from Data
&lt;/h3&gt;

&lt;p&gt;Although every organization is now aware that data is their gold mine, few have managed to establish a sound data strategy that will allow them to consistently monetize from it. Most companies have trouble simplifying their data architectures to build a single layer that augments all the relevant customer related information in one central location. Connecting with leads and channelizing efforts to truly convert engagement to revenue is that piece of the puzzle that unfortunately many are still trying to solve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Which are the alternatives to Google Analytics
&lt;/h2&gt;

&lt;p&gt;There a lot of tools on the Internet that can help you better and more deeply analyze your SAAS application.&lt;/p&gt;

&lt;p&gt;The most popular are three (in simple alphabetical order):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amplitude (&lt;a href="https://amplitude.zendesk.com/hc/en-us/sections/201146908-Amplitude-Quick-Start-Guide"&gt;Getting Started Guide&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;MixPanel (&lt;a href="https://help.mixpanel.com/hc/en-us/categories/115001031023-Get-Started"&gt;Getting Started Guide&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Woopra (&lt;a href="https://docs.woopra.com/docs/getting-started-guide-the-woopra-essentials-mix"&gt;Getting Started Guide&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They differ for the price, but the features are quite the same: track events, track customers and combine data in useful reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think at tracking when you think at your product
&lt;/h2&gt;

&lt;p&gt;Tracking is not something to think about later, when your product is built and ready to be shipped (really early! Does the acronym MVP sound familiar?).&lt;/p&gt;

&lt;p&gt;During the development of your product, you need to think at the permissions of your users: each set of permissions usually links with a journey (a funnel) and with an actor.&lt;/p&gt;

&lt;p&gt;You have to merge the tracking part with the development part: still during development, when you design your internal flows, you have to highlight the KPIs: how many visitors became users? How many users became customers? You are understanding the song, don’t you?&lt;/p&gt;

&lt;p&gt;In doing this, you need to also consider privacy.&lt;/p&gt;

&lt;p&gt;This is called &lt;a href="https://en.wikipedia.org/wiki/Privacy_by_design"&gt;Privacy by Design&lt;/a&gt; and is a fundamental part of your product: the GDPR requires you to have precise documents that explain what, why, how and for how much time you treat personal data.&lt;/p&gt;

&lt;p&gt;This job is useful also for your documentation: you need to explain to your customers how to solve their problems using your products.&lt;/p&gt;

&lt;p&gt;All this without speaking about marketing (not only content marketing and blogging).&lt;/p&gt;

&lt;p&gt;So, thinking at tracking already during the design phases of your SAAS product reverberates its beneficial effects on at least five areas of your startup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Flow design and usage paths;&lt;/li&gt;
&lt;li&gt;Measurement;&lt;/li&gt;
&lt;li&gt;Privacy;&lt;/li&gt;
&lt;li&gt;Documentation;&lt;/li&gt;
&lt;li&gt;Marketing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not a win-win, this is a jackpot!?&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing for tracking
&lt;/h2&gt;

&lt;p&gt;So, you are already doing the hard parts of the tracking instrumentation.&lt;/p&gt;

&lt;p&gt;Below there is a step by step flow you can follow: it will turn useful also for developers and your product team: They will love you for this! &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uJjtDQHw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/2764.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uJjtDQHw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/2764.png" alt="❤" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  List your actors
&lt;/h3&gt;

&lt;p&gt;Any person on your app can do a lot of things.&lt;/p&gt;

&lt;p&gt;Now, some user can follow some journeys, some others can follow some other journeys.&lt;/p&gt;

&lt;p&gt;When a user can follow a particular journey, we call him/her an Actor.&lt;/p&gt;

&lt;p&gt;Practically, an actor is a User who is making a journey to do something.&lt;/p&gt;

&lt;p&gt;For example, on &lt;a href="https://www.trustback.me"&gt;TrustBack.Me&lt;/a&gt; the journeys that a User who is a Merchant can follow are different than the journeys that a User who is a Customer can follow. Incidentally, a User who is a Merchant can be also a Customer.&lt;/p&gt;

&lt;p&gt;So, you should list your Actors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Highlight the journeys (flows, paths, funnels)
&lt;/h3&gt;

&lt;p&gt;Now that you have a clear picture of your Actors, you can go to the next step: identify the journeys they can make.&lt;/p&gt;

&lt;p&gt;In your application, any user can do various things to accomplish a task.&lt;/p&gt;

&lt;p&gt;This is commonly called a flow (the devs call it that), or a funnel (more salesy term) or path or a “journey”: yes, this last term is the one we prefer.&lt;/p&gt;

&lt;p&gt;The term “flow” is aseptic, so logical.&lt;/p&gt;

&lt;p&gt;The term “funnel” has implicitly a connotation of sell: you have sale funnel, the lead capturing funnel and so on.&lt;/p&gt;

&lt;p&gt;The term “journey” perfectly highlights the experience your users experience using your product.&lt;/p&gt;

&lt;p&gt;After all, the UX is the first thing you should take care of in building your SAAS application!&lt;/p&gt;

&lt;p&gt;So, coming back to our main point, some journeys can be followed by some users, while some other journeys can be followed by some other users.&lt;/p&gt;

&lt;p&gt;For example, in Facebook, a User can post a status, upload a photo, comment on a status or a photo, leave a Like, ecc.: all those things are a journey (or a flow from the development perspective).&lt;/p&gt;

&lt;p&gt;So, the first thing you should do is to highlight the possible journeys your users can experience in your application.&lt;/p&gt;

&lt;p&gt;Amplitude has a good &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/360000748812-For-New-Users-Getting-Started#kickoff"&gt;Kickoff video&lt;/a&gt;  (from about minute 3) and a &lt;a href="https://amplitude.zendesk.com/hc/en-us/articles/115000465251#what-are-my-critical-paths"&gt;concrete example&lt;/a&gt; that explain exactly how to identify the main journeys users can take in your app.&lt;/p&gt;

&lt;p&gt;To map your journeys you can use a tool like &lt;a href="https://www.draw.io/"&gt;Draw.io&lt;/a&gt; or &lt;a href="https://realtimeboard.com"&gt;Realtime Board&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wYQXzQqw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/FB-reporting-flow-min.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wYQXzQqw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2018/10/FB-reporting-flow-min.jpg" alt="" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Identify key events in journeys
&lt;/h3&gt;

&lt;p&gt;Each journey fires some events: those are the events you are going to track and measure to see how they relate and which frictions exist in them.&lt;/p&gt;

&lt;p&gt;Once you have your journeys highlighted, you need to understand which users can follow which journeys.&lt;/p&gt;

&lt;p&gt;When a User follows a journey (s)he is not a user anymore: (s)he is an actor as (s)he acting like a particular kind of user.&lt;/p&gt;

&lt;p&gt;For example, in Facebook, a Merchant who is promoting his/her page, act differently from a User who posts a status.&lt;/p&gt;

&lt;p&gt;And also if the posting of a status can be done by a Merchant on his/her page or by a user on his/her own profile, those are two completely different journeys.&lt;/p&gt;

&lt;p&gt;So, the next step is to give a name as an actor to each user who is following a journey.&lt;/p&gt;

&lt;p&gt;For example, on TrustBack.Me, among the others, a User can be a Merchant or a Customer (someone who bought something on the store of a Merchant).&lt;/p&gt;

&lt;h3&gt;
  
  
  Highlight the relations between journeys
&lt;/h3&gt;

&lt;p&gt;Journeys are not standalone sequences of events.&lt;/p&gt;

&lt;p&gt;Practically, each journey can lead to another or to more than one another journey.&lt;/p&gt;

&lt;p&gt;For example, on TrustBack.Me, the Reclaim Domain journey leads to the Getting Started journey which goal is to lead the Merchant to configure the connection between TrustBack.Me and his/her e-commerce store.&lt;/p&gt;

&lt;p&gt;We also do our best efforts to make a buyer who releases a feedback (release a feedback journey) become a Consumer, following the Hard Email Reclaim journey: the two are linked. And once the Buyer becomes a Customer, we do our best to understand if we can make (s)he transition to the Merchant Actor figure following the Domain reclaim procedure (if applicable).&lt;/p&gt;

&lt;h3&gt;
  
  
  Highlight the transitions between actors
&lt;/h3&gt;

&lt;p&gt;Continuing to navigate your journeys, any user takes the clothes of different actors.&lt;/p&gt;

&lt;p&gt;So, you need to understand the possible transitions between those actors figures.&lt;/p&gt;

&lt;p&gt;For example, with TrustBack.Me our interest is to make it happen that the actor Visitor transitions to the actor Merchant.&lt;/p&gt;

&lt;p&gt;Or that a simple Buyer transitions to the actor Consumer.&lt;/p&gt;

&lt;p&gt;When analyzing your data, you can track the progress toward your app’s main goals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusions
&lt;/h3&gt;

&lt;p&gt;This was only a birds eye in your journey to implement a behavioural analytics tool to track your application.&lt;/p&gt;

&lt;p&gt;There are many other things you need to consider, write and do before you are ready to fully implement and understand how to use a tracking tool like this.&lt;/p&gt;

&lt;p&gt;Anyway, you now should have a better understanding of the differences between Google Analytics and behavioural analytics tools: Google Analytics focuses on Page Views; behavioural analytics tools focus on people and the actions they take in your app (events).&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/behavioural-analytics/"&gt;How to start using behavioural analytics tools to track your SAAS product&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to manage static images with Symfony’s Webpack Encore</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Fri, 06 Apr 2018 09:02:57 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-manage-static-images-with-symfonys-webpack-encore-31h1</link>
      <guid>https://dev.to/serendipityhq/how-to-manage-static-images-with-symfonys-webpack-encore-31h1</guid>
      <description>&lt;p&gt;Any web project has basically 4 kind of places where images reside:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the CSS;&lt;/li&gt;
&lt;li&gt;In a font file (think at icons);&lt;/li&gt;
&lt;li&gt;In the cloud (think at AWS S3);&lt;/li&gt;
&lt;li&gt;In the project itself as static files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Symfony’s Webpack Encore is great to manage the first two places as it is able to manage the images referenced in the CSSes and to manage fonts (as they are handled as CSSes are).&lt;/p&gt;

&lt;p&gt;The third group of images, the ones in the cloud, have not to be managed by Webpack but by another bundle like &lt;a href="http://aerendir.me/2016/01/25/how-to-use-liipimaginebundle-to-manage-thumbnails-through-amazon-s3/"&gt;LiipImagineBundle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So we are left only with the fourth group: the static images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which is the problem with static images
&lt;/h2&gt;

&lt;p&gt;When you want to use an image in your Twig templates (or any other asset) you are required to use the &lt;code&gt;asset()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{# templates/stylesheets.html.twig #}
{% block stylesheets %}
     &amp;lt;link href="{{ asset('build/app_global.css') }}" rel="stylesheet" /&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the path references the &lt;code&gt;build&lt;/code&gt; folder: its full path is &lt;code&gt;public/build&lt;/code&gt; and it is created by Webpack Encore when running &lt;code&gt;node_modules/.bin/encore dev&lt;/code&gt; (or simply &lt;code&gt;yarn dev&lt;/code&gt; if you have the script set in your &lt;code&gt;package.json&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This command does some simple things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the &lt;code&gt;webpack.config.js&lt;/code&gt; file;&lt;/li&gt;
&lt;li&gt;Combines all &lt;code&gt;css&lt;/code&gt; and &lt;code&gt;scss&lt;/code&gt; into one file and puts it in the &lt;code&gt;public/build&lt;/code&gt; folder;&lt;/li&gt;
&lt;li&gt;Finds all the images referenced in the &lt;code&gt;css&lt;/code&gt; and copies them in the folder &lt;code&gt;public/build/path/to/image&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Eventually minifies the generated &lt;code&gt;css&lt;/code&gt; (if you configured the minification);&lt;/li&gt;
&lt;li&gt;Combines all &lt;code&gt;js&lt;/code&gt; files into one file;&lt;/li&gt;
&lt;li&gt;Eventually minifies the generated &lt;code&gt;js&lt;/code&gt; (if you configured the minification);&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then you can include the &lt;code&gt;js&lt;/code&gt; and &lt;code&gt;css&lt;/code&gt; files using the &lt;code&gt;asset()&lt;/code&gt; function as shown above.&lt;/p&gt;

&lt;p&gt;But running &lt;code&gt;node_modules/.bin/encore dev&lt;/code&gt; doesn’t copy the images referenced in the Twig templates through the &lt;code&gt;asset()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;For instance, if you have a code like this, the images will not be copied in the &lt;code&gt;build/images/privacy&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="privacy text-center"&amp;gt;
    &amp;lt;span ...&amp;gt;
        &amp;lt;img src="{{ asset('build/images/privacy/noSpam.png') }}" ... /&amp;gt;
    &amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is a problem as we practically cannot reference them through the &lt;code&gt;asset()&lt;/code&gt; function!&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Symfony Best Practices tell about managing static images with Webpack Encore
&lt;/h2&gt;

&lt;p&gt;Nothing! The &lt;a href="http://symfony.com/doc/current/best_practices/web-assets.html"&gt;best practices&lt;/a&gt; don’t mention at all the static images, nor they have a section dedicated to them.&lt;/p&gt;

&lt;p&gt;Better, they tell this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Web assets are things like CSS, JavaScript and image files that make the frontend of your site look and work great.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, they reference to the &lt;a href="https://symfony.com/doc/current/frontend.html"&gt;Symfony’s Webpack Encore documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But this documentation doesn’t mention at all the handling of images: only the handling of CSS and JavaScript.&lt;/p&gt;

&lt;p&gt;So, basically, in this moment there is no best practice in place to manage static images nor a documentation that explains how to manage them.&lt;/p&gt;

&lt;p&gt;But static images are a big part of a web project as not all images have to be referenced in CSS nor have to be in a font or in the cloud.&lt;/p&gt;

&lt;p&gt;We need a solution to manage static images with Symfony’s Webpack Encore!&lt;/p&gt;

&lt;p&gt;So, here it is: lets see it!&lt;/p&gt;

&lt;h2&gt;
  
  
  How to manage static image files with Symfony’s Webpack Encore
&lt;/h2&gt;

&lt;p&gt;Fortunately the Symfony’s community is very large and active, so, sifting both StackOverflow and the Symfony’s issues on GitHub it is possible to reconstruct a solution to manage static images with Webpack.&lt;/p&gt;

&lt;p&gt;The two main discussion about this problem are these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/Haehnchen/idea-php-symfony2-plugin/issues/1020"&gt;Missing assets in Twig autocompletation when using manifest.json and Webpack-Encore versioning (Haehnchen/idea-php-symfony2-plugin issues page)&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/symfony/webpack-encore/issues/24"&gt;How are static assets handled? (symfony/webpack-encore issues page)&lt;/a&gt;;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In these discussion there is the full solution to manage static images with Symfony’s Webpack Encore.&lt;/p&gt;

&lt;p&gt;Lets see them one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first (ugly and messy) approach: putting all images directly in the &lt;code&gt;templates/images&lt;/code&gt; folder
&lt;/h3&gt;

&lt;p&gt;The simplest way to make the &lt;code&gt;asset()&lt;/code&gt; function is to simply put the static images directly in the &lt;code&gt;public/build/images&lt;/code&gt; folder: this way the function is able to find them and reference them.&lt;/p&gt;

&lt;p&gt;But this approach, although very simple, has some major drawbacks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You have your assets in two different places: in the &lt;code&gt;assets&lt;/code&gt; folder and directly in the &lt;code&gt;public&lt;/code&gt; folder (and also in the &lt;code&gt;templates&lt;/code&gt; folder for twig templates: three different places!)&lt;/li&gt;
&lt;li&gt;You cannot use versioning as Webpack Encore is not aware of these images and so cannot put them in the &lt;code&gt;manifest.json&lt;/code&gt; file (more about soon).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, if you don’t use versioning and you are ok using a messy approach, go this way and put your static images directly in the &lt;code&gt;public/build/images&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;I don’t like messy things and I want to use versioning, so I want a more clear and organized way.&lt;/p&gt;

&lt;h3&gt;
  
  
  The second (tiring) approach: requiring images through &lt;code&gt;require&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The basic concept here is that to make Webpack aware of images.&lt;/p&gt;

&lt;p&gt;So, basically, we have to require all the images using &lt;code&gt;require&lt;/code&gt; in our JavaScript files: this way Webpack can move them to the &lt;code&gt;public/build/images&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// assets/images.js
require('./images/privacy/noSpam.png');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you have to import this file in one of your already existent files so it can be processed by Webpack Encore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// assets/js/_main.js
require('../../images');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way you will have the file &lt;code&gt;noSpam.png&lt;/code&gt; copied in &lt;code&gt;public/build/images/noSpam.d47c971d.png&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;As you can see it was &lt;a href="https://symfony.com/doc/current/frontend/encore/versioning.html"&gt;versioned&lt;/a&gt; and it was also added to the &lt;code&gt;manifest.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  ...
  "build/images/noSpam.png": "/build/images/noSpam.d47c971d.png",
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach was &lt;a href="https://github.com/symfony/webpack-encore/issues/24#issuecomment-313209746"&gt;suggested by Ryan Weaver&lt;/a&gt;, the author of Webpack Encore, and is already a solution but its major drawback is that we have to create a file to require images and also require any image we want to use in our Twig templates: this means extra work and also an error prone one as we may forget to add the image, or, anyway, we have to explain to anyone who comes working on the project that (s)he has to add the images to the file, and we the file name changes we have to update it in the &lt;code&gt;images.js&lt;/code&gt; file too, and if we decide to add 10 more images we have to require each one of them, and if…&lt;/p&gt;

&lt;p&gt;Definitely, this is a solution, but not a good one: we need something better.&lt;/p&gt;

&lt;h3&gt;
  
  
  The third (ÐΞV’s way) approach: introducing &lt;code&gt;require.context&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;What we want is to simply put all the images we need into a folder and then let Webpack Encore do the rest, without any further action on our side.&lt;/p&gt;

&lt;p&gt;So we need a way to make Webpack Encore able to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan a folder recursively;&lt;/li&gt;
&lt;li&gt;Find the image files;&lt;/li&gt;
&lt;li&gt;Move them to the &lt;code&gt;public/images&lt;/code&gt; folder;&lt;/li&gt;
&lt;li&gt;Add them to the &lt;code&gt;manifest.json&lt;/code&gt; file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But, how can we achieve this?&lt;/p&gt;

&lt;p&gt;The solution is &lt;a href="https://github.com/webpack/docs/wiki/context#requirecontext"&gt;&lt;code&gt;require.context&lt;/code&gt;&lt;/a&gt; as &lt;a href="https://github.com/symfony/webpack-encore/issues/24#issuecomment-362927213"&gt;suggested by Vincent Le Biannic (aka Lyrkan)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The code is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// assets/js/_main.js
const imagesContext = require.context('../images', true, /\.(png|jpg|jpeg|gif|ico|svg|webp)$/);
imagesContext.keys().forEach(imagesContext);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;require.context&lt;/code&gt; call creates a custom context in Webpack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first argument is the folder to scan;&lt;/li&gt;
&lt;li&gt;The second argument indicates to scan also the subfolders;&lt;/li&gt;
&lt;li&gt;The third argument is a regular expression used to match the file names we want to include in the context.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the &lt;code&gt;imagesContext.keys().forEach&lt;/code&gt; cycle through the result and require each found element.&lt;/p&gt;

&lt;p&gt;Really simple!&lt;/p&gt;

&lt;p&gt;The result is that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All found images are copied in the path &lt;code&gt;public/build/images/&lt;/code&gt; folder (in a flatten way, without taking care of the original subfolder: it is not included in the resulting path);&lt;/li&gt;
&lt;li&gt;All fund images are also added to the &lt;code&gt;manifest.json&lt;/code&gt; file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the resulting &lt;code&gt;manifest.json&lt;/code&gt; file (at least a small portion of it ?):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  ...
  "build/images/NoAdvertisers.png": "/build/images/NoAdvertisers.97d35611.png",
  "build/images/Unsubscribe.png": "/build/images/Unsubscribe.c63a3aac.png",
  "build/images/noSold.png": "/build/images/noSold.2b558443.png",
  "build/images/noSpam.png": "/build/images/noSpam.d47c971d.png",
  "build/images/onlyIntendedUse.png": "/build/images/onlyIntendedUse.ac6db9bf.png",
  "build/images/timeIndefinite.png": "/build/images/timeIndefinite.45f77ed9.png",
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This solution is very close to what we need but it has a (maybe minor) drawback, too: the files are copied all into the &lt;code&gt;public/images&lt;/code&gt; folder, without reflecting the full path: this maybe an issue if you have two or more images with the same name in different folders and you are not using the versioning (enabling the versioning, anyway, &lt;a href="https://github.com/symfony/webpack-encore/issues/73#issuecomment-312492867"&gt;solves the problem&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;But also if you can solve the “same name” problem, remains the fact that having the same folder structure maybe helpful to find on the fly the image in the &lt;code&gt;assets&lt;/code&gt; folder: not fundamental, but certainly useful.&lt;/p&gt;

&lt;p&gt;So, a bit of syntactic sugar to reflect the folder structure is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// webpack.config.js
const Encore = require('@symfony/webpack-encore');

Encore
 // directory where all compiled assets will be stored
 .setOutputPath('public/build/')

 // Relative to your project's document root dir
 .setPublicPath('/build')

 // empty the outputPath dir before each build
 .cleanupOutputBeforeBuild()

 // Here all other required configurations
 ...

 .configureFilenames({
 images: '[path][name].[hash:8].[ext]',
 })
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doing this will reflect in &lt;code&gt;public/build&lt;/code&gt; the folder structure of &lt;code&gt;assets&lt;/code&gt; producing something like &lt;code&gt;public &amp;gt; build &amp;gt; assets &amp;gt; images &amp;gt; sub_folder &amp;gt; you_image.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As you can see it anyway include the folder &lt;code&gt;asset&lt;/code&gt; but &lt;a href="https://github.com/symfony/webpack-encore/issues/24#issuecomment-363030622"&gt;removing it requires too much work&lt;/a&gt; and the effort is not worth the benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We have seen how to manage static images with Webpack in a convenient way.&lt;/p&gt;

&lt;p&gt;We have explored three ways of doing it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;hand-putting the images directly in the &lt;code&gt;public/build&lt;/code&gt; folder (messy);&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;require&lt;/code&gt; for each image we want to be moved in &lt;code&gt;public/build&lt;/code&gt; and versioned (tired);&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;require.context&lt;/code&gt; and a little bit of syntactic sugar in &lt;code&gt;webpack.config.js&lt;/code&gt; (the ÐΞV’s way).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Which one of these solutions will you choose? ?&lt;/p&gt;

&lt;p&gt;Mmm, I think I have no doubts about! ? … ?&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/managing-static-images-webpack-encore/"&gt;How to manage static images with Symfony’s Webpack Encore&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>javascript</category>
      <category>symfony</category>
      <category>webpack</category>
    </item>
    <item>
      <title>How to use the Doctrine command line developing a Symfony Bundle</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Fri, 30 Sep 2016 16:14:04 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-use-the-doctrine-command-line-developing-a-symfony-bundle-1knm</link>
      <guid>https://dev.to/serendipityhq/how-to-use-the-doctrine-command-line-developing-a-symfony-bundle-1knm</guid>
      <description>&lt;p&gt;Maybe you need to use the Doctrine’s command line while you are developing a Symfony Bundle (or anything else).&lt;/p&gt;

&lt;p&gt;This will make you able, for example, to validate the schema or to convert an annotated schema to its &lt;code&gt;XML&lt;/code&gt; counterpart.&lt;/p&gt;

&lt;p&gt;Here is how to use Doctrine command line outside of a Symfony Application.&lt;/p&gt;

&lt;p&gt;You have to simply create a &lt;code&gt;cli-config.php&lt;/code&gt; file and put it into the root directory of your developing bundle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php 

use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;

$paths = [getcwd() . "/Model"];
$isDevMode = true;

// the connection configuration
$dbParams = array( 'driver' =&amp;gt; 'pdo_mysql',
    'user' =&amp;gt; 'root',
    'password' =&amp;gt; 'root',
    'dbname' =&amp;gt; 'test_your_bundle',
);

// @see http://stackoverflow.com/a/19129147/1399706
$config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode, null, null, false);

$entityManager = EntityManager::create($dbParams, $config);

return ConsoleRunner::createHelperSet($entityManager);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use &lt;code&gt;vendor/bin/doctrine your:command&lt;/code&gt; to perform the task you need.&lt;/p&gt;

&lt;p&gt;Very simple! &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/doctrine-command-line-symfony-bundle/"&gt;How to use the Doctrine command line developing a Symfony Bundle&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>doctrine</category>
      <category>php</category>
    </item>
    <item>
      <title>Magento 2: SSL certificate problem: unable to get local issuer certificate (cURL problem)</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Fri, 29 Apr 2016 07:07:18 +0000</pubDate>
      <link>https://dev.to/serendipityhq/magento-2-ssl-certificate-problem-unable-to-get-local-issuer-certificate-curl-problem-k97</link>
      <guid>https://dev.to/serendipityhq/magento-2-ssl-certificate-problem-unable-to-get-local-issuer-certificate-curl-problem-k97</guid>
      <description>&lt;p&gt;The message “SSL certificate problem: unable to get local issuer certificate” shows up when trying to connect to Magento Connect or when, generally, you try to use &lt;code&gt;cURL&lt;/code&gt; to connect to a remote web site.&lt;/p&gt;

&lt;p&gt;This error happens because &lt;code&gt;cURL&lt;/code&gt; cannot find a &lt;code&gt;cacert.pem&lt;/code&gt; file from which take the trusted signatures.  &lt;/p&gt;

&lt;p&gt;There are some ways to set this file in &lt;code&gt;cURL&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pass the &lt;code&gt;cacert.pem&lt;/code&gt; file path directly to &lt;code&gt;cURL&lt;/code&gt; when making the call;&lt;/li&gt;
&lt;li&gt;Set the path to the &lt;code&gt;cacert.pem&lt;/code&gt; file in the &lt;code&gt;php.ini&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Other options are to set the environment variable &lt;code&gt;CURL_CA_BUNDLE&lt;/code&gt; or to put the &lt;code&gt;cacert.pem&lt;/code&gt; file in a defined directory on your filesystem depending on your OS.&lt;/p&gt;

&lt;p&gt;But, as we are working with digital certificates with &lt;code&gt;PHP cURL&lt;/code&gt;, lets use &lt;code&gt;PHP&lt;/code&gt;! :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Pass the &lt;code&gt;cacert.pem&lt;/code&gt; file path directly to &lt;code&gt;cURL&lt;/code&gt; when making the call
&lt;/h2&gt;

&lt;p&gt;To do this, simply pass the &lt;code&gt;cacert.pem&lt;/code&gt; file path as parameter to pass to &lt;code&gt;stream_context_create()&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$contextOptions = [
    'ssl' = [
        'verify_peer' = true,
        'verify_peer_name' = true,
        'allow_self_signed' = false,
        'cafile' = 'path/to/you/cacert.pem',
        'ciphers' = 'HIGH',
        'disable_compression' = true,
        'capture_peer_cert' = true,
        'capture_peer_cert_chain' = true,
        'capture_session_meta' = true,
    ]
];

$context = stream_context_create($contextOptions);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to set the path to the &lt;code&gt;cacert.pem&lt;/code&gt; file path in the &lt;code&gt;php.ini&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The other more robust solution is to set the &lt;code&gt;cacert.pem&lt;/code&gt; file path directly in the &lt;code&gt;php.ini&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To do this, find the line &lt;code&gt;curl.cainfo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[curl]
; A default value for the CURLOPT_CAINFO option. This is required to be an
; absolute path.
;curl.cainfo =

[openssl]
; The location of a Certificate Authority (CA) file on the local filesystem
; to use when verifying the identity of SSL/TLS peers. Most users should
; not specify a value for this directive as PHP will attempt to use the
; OS-managed cert stores in its absence. If specified, this value may still
; be overridden on a per-stream basis via the "cafile" SSL stream context
; option.
;openssl.cafile=

; If openssl.cafile is not specified or if the CA file is not found, the
; directory pointed to by openssl.capath is searched for a suitable
; certificate. This value must be a correctly hashed certificate directory.
; Most users should not specify a value for this directive as PHP will
; attempt to use the OS-managed cert stores in its absence. If specified,
; this value may still be overridden on a per-stream basis via the "capath"
; SSL stream context option.
;openssl.capath=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make &lt;code&gt;cURL&lt;/code&gt; work with digital certificates is sufficient to simply set the &lt;code&gt;curl.cainfo&lt;/code&gt; parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[curl]
; A default value for the CURLOPT_CAINFO option. This is required to be an
; absolute path.
curl.cainfo = /usr/local/etc/openssl/certs/cacert.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the &lt;code&gt;php.ini&lt;/code&gt; file and restart Apache. Try again and all should work well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Magento: SSL certificate problem: unable to get local issuer certificate
&lt;/h2&gt;

&lt;p&gt;Obviously, to solve the “SSL certificate problem: unable to get local issuer certificate” error in Magento when trying to connect to MagentoConnect the option we should choose is the second: set the &lt;code&gt;cacert.pem&lt;/code&gt; file path directly in the &lt;code&gt;php.ini&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to download a &lt;code&gt;cacert.pem&lt;/code&gt; file
&lt;/h2&gt;

&lt;p&gt;There isn’t an official &lt;code&gt;cacert.pem&lt;/code&gt;, so we have to use the most accredited one, that is &lt;a href="https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/"&gt;the one compiled by Mozilla&lt;/a&gt; and that can be downoaded from &lt;a href="http://curl.haxx.se/ca/cacert.pem"&gt;http://curl.haxx.se/ca/cacert.pem&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like, here you’ll find &lt;a href="http://aerendir.me/2015/07/31/a-useful-php-ini-configuration-for-local-development/"&gt;other useful &lt;code&gt;php.ini&lt;/code&gt; settings for local web development&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/magento-2-ssl-certificate-problem-unable-to-get-local-issuer-certificate-curl-problem/"&gt;Magento 2: SSL certificate problem: unable to get local issuer certificate (cURL problem)&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>magento</category>
      <category>php</category>
    </item>
    <item>
      <title>How to use LiipImagineBundle to manage thumbnails through Amazon S3</title>
      <dc:creator>Adamo Crespi</dc:creator>
      <pubDate>Mon, 25 Jan 2016 16:45:35 +0000</pubDate>
      <link>https://dev.to/serendipityhq/how-to-use-liipimaginebundle-to-manage-thumbnails-through-amazon-s3-2a6p</link>
      <guid>https://dev.to/serendipityhq/how-to-use-liipimaginebundle-to-manage-thumbnails-through-amazon-s3-2a6p</guid>
      <description>&lt;p&gt;Configure &lt;code&gt;LiipImagineBundle&lt;/code&gt; to create thumbnails of images stored on Amazon S3 and save a cache version of them again on S3 may be really painful.&lt;/p&gt;

&lt;p&gt;In this post I’ll try to guide you step by step in the configuration process, to have a full configured data flow to create thumbnails stored on Amazon S3 using &lt;code&gt;LiipImagineBundle&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which is our goal
&lt;/h2&gt;

&lt;p&gt;Our goal is to configure the following data flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a source image file from AWS S3 (from the &lt;code&gt;[bucket_name]/images&lt;/code&gt; folder);&lt;/li&gt;
&lt;li&gt;Manipulate it in some ways with &lt;code&gt;LiipImagineFilters&lt;/code&gt; (for example, the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/filters.rst#the-thumbnail-filter"&gt;&lt;code&gt;thumbnail&lt;/code&gt;&lt;/a&gt; filter);&lt;/li&gt;
&lt;li&gt;Save the manipulated image again to AWS S3 (into the &lt;code&gt;bucket_name/cache&lt;/code&gt; folder).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As the configuration of the full data flow could be difficult and prone to errors, we will use a simple configuration to reduce to minimum possibilities of issues.&lt;/p&gt;

&lt;p&gt;So we will try to get an already uploaded image from the &lt;code&gt;images&lt;/code&gt; folder in our bucket on S3 (hardcoding the path in the Twig template), resize it applying custom filters &lt;code&gt;test_medium_thumb&lt;/code&gt; and &lt;code&gt;test_small_thumb&lt;/code&gt; (that we will configure), then we will save again the resized image in the &lt;code&gt;cache&lt;/code&gt; folder into same bucket on S3.&lt;/p&gt;

&lt;p&gt;Before starting with the real configuration of the &lt;code&gt;LiipImagineBundle&lt;/code&gt; we have to prepare our test bucket and our test &lt;code&gt;route&lt;/code&gt; to simplify things.&lt;/p&gt;

&lt;p&gt;We will, in fact, create a dedicate &lt;code&gt;action&lt;/code&gt; in a &lt;code&gt;controller&lt;/code&gt; to test in the tiniest way our filters. This will help us to test in a simpler manner the funcionalities of &lt;code&gt;LiipImagineBundle&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the required bundles: &lt;code&gt;LiipImagineBundle&lt;/code&gt; and &lt;code&gt;KnpGaufretteBundle&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To make all things work we need to install and activate, besides &lt;code&gt;LiipImagineBundle&lt;/code&gt;, also &lt;code&gt;KnpGaufretteBundle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/installation.rst"&gt;install and activate &lt;code&gt;LiipImagineBundle&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/KnpLabs/KnpGaufretteBundle#installation"&gt;install and activate &lt;code&gt;KnpGaufretteBundle&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/KnpLabs/Gaufrette"&gt;Gaufrette&lt;/a&gt; is a PHP5 library that provides a filesystem abstraction layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gaufrette is required to make &lt;code&gt;LiipImagineBundle&lt;/code&gt; to get and save files from and to Amazon S3 buckets.&lt;/p&gt;

&lt;p&gt;For the moment believe me and install the bundles: continuing in reading of this post you’ll better understand why we need also &lt;code&gt;KnpGaufretteBundle&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare our app for the test
&lt;/h2&gt;

&lt;p&gt;Now that we have installed and activated our required bundles, lets prepare our app to test the data flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create in your bucket the folder &lt;code&gt;images&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Into this &lt;code&gt;images&lt;/code&gt; folder upload a big image (I used &lt;a href="https://www.symfony.fi/files/2015-05/symfony2.png"&gt;this&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;cache&lt;/code&gt; folder in your bucket;&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;DefaultController::TestThumbAction&lt;/code&gt; in your &lt;code&gt;DefaultController&lt;/code&gt; (or in another controller: this action will be used to just render the test image and its thumbnail) that return the path of the just MANUALLY uploaded test image:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    /**
     * @Route("/test_thumb", name="thumb")
     * @Template()
     */
    public function testThumbAction()
    {
        $image = 'https://s3-'.
            $this-&amp;gt;getParameter('amazon.s3.region').
            '.amazonaws.com/'.
            $this-&amp;gt;getParameter('amazon.s3.bucket').
            '/images/symfony2.png';

        return [
            'image' =&amp;gt; $image
        ];
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create the template for the &lt;code&gt;DefaultController::TestThumbnailAction&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_small_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
{# NOTE THAT FOR THE MOMENT THE THUMBNAIL IS COMMENTED #}
{# &amp;lt;img src="{{ image | imagine_filter('test_small_thumb') }}" alt="" /&amp;gt; #}

&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_medium_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
{# NOTE THAT FOR THE MOMENT THE THUMBNAIL IS COMMENTED #}
{# &amp;lt;img src="{{ image | imagine_filter('test_medium_thumb') }}" alt="" /&amp;gt; #}

&amp;lt;h1&amp;gt;Original Image&amp;lt;/h1&amp;gt;
&amp;lt;img src="{{ image }}" alt="" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now go to &lt;code&gt;http://127.0.0.1:8000/test_thumb&lt;/code&gt; and check that the test image is loaded correctly.&lt;/p&gt;

&lt;p&gt;If the test image is not loaded, may be you have to set it as &lt;code&gt;public&lt;/code&gt; on AWS S3 (select the image and in the &lt;code&gt;Actions&lt;/code&gt; dropdown select &lt;code&gt;Make Public&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Once you can see the test image in its original size, remove comments from the image with &lt;code&gt;test_small_thumb&lt;/code&gt; filter applied (only from this for the moment!) and reload the page.&lt;/p&gt;

&lt;p&gt;You’ll see this &lt;code&gt;Twig_Error_Runtime&lt;/code&gt; exception&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An exception has been thrown during the rendering of a template (“Could not find configuration for a filter: test_small_thumb”) in src/AppBundle/Resources/views/Default/testThumb.html.twig at line 2.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;500&lt;/strong&gt; Internal Server Error – Twig_Error_Runtime&lt;br&gt;&lt;br&gt;
&lt;strong&gt;1&lt;/strong&gt; linked Exception: NonExistingFilterException »&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ok, we are now ready to start configuring &lt;code&gt;LiipImagineBundle&lt;/code&gt; to create our thumbnails of images stored on Amazon S3 and save the cached thumbnail again on Amazon S3. Lets start!&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the dataflow of &lt;code&gt;LiipImagineBundle&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The dataflow followed by &lt;code&gt;LiipImagineBundle&lt;/code&gt; is described in the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/introduction.rst#basic-data-flow"&gt;documentation&lt;/a&gt;. I suggest you to read it to fully understand what we need to make the thumbnails being generated and saved to S3.&lt;/p&gt;

&lt;p&gt;The short story is this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;STEP 1: &lt;code&gt;LiipImagineBundle&lt;/code&gt; &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/introduction.rst#retrieving-the-original-image"&gt;retrieve the original image&lt;/a&gt; from S3 using a &lt;code&gt;DataLoader&lt;/code&gt;.
Our &lt;code&gt;DataLoader&lt;/code&gt; is the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/data-loader/stream.rst"&gt;&lt;code&gt;StreamLoader&lt;/code&gt;&lt;/a&gt; (that uses Gaufrette to work).&lt;/li&gt;
&lt;li&gt;STEP 2: The source image is then &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/introduction.rst#apply-filters-on-the-original-image"&gt;processed&lt;/a&gt; with &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/filters.rst"&gt;filters&lt;/a&gt;.
The filters we will configure are &lt;code&gt;test_medium_thumb&lt;/code&gt; and &lt;code&gt;test_small_thumb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;STEP 3: The resulting image file is then &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/introduction.rst#cache-the-filtered-image"&gt;saved (cached)&lt;/a&gt; to S3 using a &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/cache-resolvers.rst"&gt;&lt;code&gt;CacheResolver&lt;/code&gt;&lt;/a&gt;.
The &lt;code&gt;CacheResolver&lt;/code&gt; we’ll use is the &lt;code&gt;AwsS3Resolver.&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  STEP 1: Get the image source file from AWS S3 with Gaufrette
&lt;/h2&gt;

&lt;p&gt;So, as told, the first step is to get the source image from the &lt;code&gt;images&lt;/code&gt; folder in our bucket on AWS S3.&lt;/p&gt;

&lt;p&gt;To do this, &lt;code&gt;LiipImagnineBundle&lt;/code&gt; requires a &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/data-loaders.rst"&gt;&lt;code&gt;DataLoader&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LiipImagnineBundle&lt;/code&gt; comes with some bundled &lt;a href="https://github.com/liip/LiipImagineBundle/tree/master/Resources/doc/data-loader"&gt;&lt;code&gt;DataLoader&lt;/code&gt;s&lt;/a&gt;. The default &lt;code&gt;DataLoader&lt;/code&gt; is the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/data-loader/filesystem.rst"&gt;&lt;code&gt;FilesystemLoader&lt;/code&gt;&lt;/a&gt; as described also in the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/configuration.rst"&gt;Configuration chapter&lt;/a&gt; of the documentation.&lt;/p&gt;

&lt;p&gt;To load images from Amazon S3, we need another &lt;code&gt;DataLoader&lt;/code&gt;, the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/data-loader/stream.rst"&gt;&lt;code&gt;StreamLoader&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;Liip\ImagineBundle\Binary\Loader\StreamLoader&lt;/code&gt; allows to read images from any stream (http, ftp, and others…) registered thus allowing you to serve your images from literally anywhere.&lt;/p&gt;

&lt;cite title="LiipImagineBundle Documentation, Stream Loade"&gt;LiipImagineBundle Documentation, Stream Loader&lt;/cite&gt;
&lt;/blockquote&gt;

&lt;p&gt;The easier way to use this loader is configuring it to use &lt;a href="https://github.com/KnpLabs/Gaufrette"&gt;Gaufrette&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So we have to first configure Gaufrette and then use it with the &lt;code&gt;LiipImagineBundle&lt;/code&gt;‘&lt;code&gt;StreamLoader&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand how Gaufrette works
&lt;/h3&gt;

&lt;p&gt;At this point Gaufrette should be already installed as you should have already installed and activated &lt;code&gt;KnpGaufretteBundle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But, before continuing, just a little bit of theory to understand how Gaufrette works.&lt;/p&gt;

&lt;p&gt;As told before,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gaufrette is a PHP 5.3+ library providing a filesystem abstraction layer. This abstraction layer allows you to develop applications without needing to know where all their media files will be stored or how.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, you interact with Gaufrette’s API that emulates actions you can take on a filesystem to manage files, so, when you decide to switch from the local filesystem to a cloud environment, you have to simply change some parameters in the configuration. Clean, simple and easy!&lt;/p&gt;

&lt;p&gt;If you like, read &lt;a href="http://knplabs.com/en/blog/give-your-projects-a-gaufrette"&gt;some more details&lt;/a&gt; on the Knp Labs’ blog.&lt;/p&gt;

&lt;p&gt;Anyway, to make your application “storage agnostic”, Gaufrette uses two main concepts: &lt;code&gt;Adapters&lt;/code&gt; and &lt;code&gt;Filesystems&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your application will ever communicate with a &lt;code&gt;Filesystem&lt;/code&gt; object. An &lt;code&gt;Filesystem&lt;/code&gt; object uses a &lt;code&gt;Adapter&lt;/code&gt; to communicate with the “storage” and perform the real actions on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/KnpLabs/Gaufrette#setup-your-filesystem"&gt;Here the details&lt;/a&gt; about this simple process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing Gaufrette &lt;code&gt;StreamWrapper&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To make &lt;code&gt;LiipImagineBundle&lt;/code&gt; &lt;code&gt;StreamLoader&lt;/code&gt; work with images we need to understand another concept used by Gaufrette, the last one: &lt;code&gt;StreamWrapper&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As we don’t simply want to interact with the filesystem, but we want also to manipulate images, we need a &lt;a href="https://github.com/KnpLabs/Gaufrette#streaming-files"&gt;&lt;code&gt;StreamWrapper&lt;/code&gt;&lt;/a&gt; that we’ll then use as &lt;code&gt;DataLoader&lt;/code&gt; in &lt;code&gt;LiipImagineBundle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;StreamWrapper&lt;/code&gt; permits to &lt;a href="http://php.net/manual/en/function.stream-wrapper-register.php"&gt;register a new stream wrapper&lt;/a&gt; so we’ll can get the contents of the image and use them to create a new image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring &lt;code&gt;KnpGaufretteBundle&lt;/code&gt; to work with AWS S3
&lt;/h3&gt;

&lt;p&gt;So, after the theory, finally the practice! Lets configure &lt;code&gt;KnpGaufretteBundle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, we need to configure those three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An &lt;code&gt;Adapter&lt;/code&gt;: we’ll use the &lt;a href="https://github.com/KnpLabs/KnpGaufretteBundle#awss3"&gt;&lt;code&gt;AwsS3Adapter&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Filesystem&lt;/code&gt; that uses the &lt;code&gt;AwsS3Adapter&lt;/code&gt;: we’ll call it &lt;code&gt;filesystem_aws_S3_images&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;StreamWrapper&lt;/code&gt; (we don’t have to do anything more than writing a directive in our &lt;code&gt;config.yml&lt;/code&gt; file to configure it)&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Configure the AwsS3Adapter for &lt;code&gt;KnpGaufretteBundle&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;AwsS3Adapter&lt;/code&gt; &lt;a href="https://github.com/KnpLabs/Gaufrette#using-amazon-s3"&gt;needs the credentials&lt;/a&gt; to access the bucket on Amazon AWS S3, so, we first configure these credentials, then we’ll configure the &lt;code&gt;S3Client&lt;/code&gt; (that uses the credentials):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shq.amazon.s3Credentials:
    class: Aws\Credentials\Credentials
    arguments: ["%amazon.s3.key%", "%amazon.s3.secret%"]

shq.amazon.s3:
    class: Aws\S3\S3Client
    arguments:
        - version: %amazon.s3.version%
          region: %amazon.s3.region%
          credentials: "@shq.amazon.s3Credentials"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can configure the &lt;code&gt;AwsS3Adapter&lt;/code&gt;, the &lt;code&gt;Filesystem&lt;/code&gt; and the &lt;code&gt;StreamWrapper&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# config.yml
# KnpGaufretteBundle Configuration
knp_gaufrette:
    adapters:
        adapter_aws_s3_images:
            aws_s3:
                service_id: "shq.amazon.s3" # ! ! ! Without @ ! ! !
                bucket_name: "%amazon.s3.bucket%"
                options:
                    directory: 'images'
                    create: true
    filesystems:
        filesystem_aws_s3_images:
            adapter: adapter_aws_s3_images
    stream_wrapper: ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As told in the &lt;a href="https://github.com/KnpLabs/KnpGaufretteBundle#stream-wrapper"&gt;documentation&lt;/a&gt;,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;stream_wrapper&lt;/code&gt; settings allow you to register filesystems with a specified domain and then use as a stream wrapper anywhere in your code like: &lt;code&gt;gaufrette://domain/file.txt&lt;/code&gt;&lt;/p&gt;

&lt;cite title="GaufretteBundle Documentation, Stream Wrappers"&gt;GaufretteBundle Documentation, Stream Wrappers&lt;/cite&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Testing that &lt;code&gt;filesystem_aws_s3_images&lt;/code&gt; works
&lt;/h4&gt;

&lt;p&gt;Now that we have configured our &lt;code&gt;Adapter&lt;/code&gt; and our &lt;code&gt;Filesystem&lt;/code&gt;, we should test that it really works as expected.&lt;/p&gt;

&lt;p&gt;Once configured, &lt;code&gt;Filesystem&lt;/code&gt;s are accessibile from the container, so your &lt;code&gt;DefaultController::testThumbAction()&lt;/code&gt; write the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    /**
     * @Route("/test_thumb", name="thumb")
     * @Template()
     */
    public function testThumbAction()
    {
        $filesystem = $this-&amp;gt;get('knp_gaufrette.filesystem_map')-&amp;gt;get('filesystem_aws_s3_images');
        $file = $filesystem-&amp;gt;get('symfony2.png');

        /*
         * Note the use of the dump() function.
         * If you don't have the VarDumperComponent installed, use var_dump().
         * @see http://symfony.com/doc/current/components/var_dumper/introduction.html
         */
        dump($file);die;

        $image = 'https://s3-'.

        ...
    }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;dump()&lt;/code&gt; shows you a &lt;code&gt;Gaufrette\File&lt;/code&gt; object, then… CONGRATULATIONS! Gaufrette is correctly configured! &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we know that Gaufrette works, we can now use it with &lt;code&gt;LiipImgineBundle&lt;/code&gt;!&lt;/p&gt;

&lt;h4&gt;
  
  
  Using Gaufrette as &lt;code&gt;DataLoader&lt;/code&gt; for &lt;code&gt;LiipImagineBundle&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;As told, we need a &lt;code&gt;DataLoader&lt;/code&gt; to make &lt;code&gt;LiipImagineBundle&lt;/code&gt; get the real content of the file so it can create a thumbnails.&lt;/p&gt;

&lt;p&gt;Now that we have a working Gaufrette, we need to use it to configure the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/data-loader/stream.rst#streamloader"&gt;&lt;code&gt;StreamLoader&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We have to alternatives to configure a &lt;code&gt;DataLoader&lt;/code&gt;: using the factory and using a defined service.&lt;br&gt;&lt;br&gt;
We’ll use the factory method, so we have all configuration in one place (I find this more comfortable, but you can use the method you like).&lt;/p&gt;

&lt;p&gt;So, to define the &lt;code&gt;DataLoader&lt;/code&gt; using the &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/data-loader/stream.rst#using-factory"&gt;factory method&lt;/a&gt;, put this in your &lt;code&gt;config.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;liip_imagine:
    loaders:
        loader_aws_s3_images:
            stream:
                # This refers to knp_gaufrette filesystems configuration
                wrapper: gaufrette://filesystem_aws_s3_images/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect: we have done. Now we have to configure custom filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: Define filters to resize images and create the thmbnails
&lt;/h2&gt;

&lt;p&gt;Now that we can get images from AWS S3, it’s time to define our custom filters to manipulate them.&lt;/p&gt;

&lt;p&gt;We will create two filters: &lt;code&gt;test_medium_thumb&lt;/code&gt; and &lt;code&gt;test_small_thumb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To define filters we have to edit the &lt;code&gt;config.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;liip_imagine:
    loaders:
...
    filter_sets:
        test_medium_thumb:
            data_loader: loader_aws_s3_images
            # We don't yet have a cache resolver configured
            cache: cache_resolver_aws_s3
            quality: 75
            filters:
                thumbnail: { size: [400, 400], mode: outbound }
        test_small_thumb:
            data_loader: loader_aws_s3_images
            # We don't yet have a cache resolver configured
            cache: cache_resolver_aws_s3
            quality: 75
            filters:
                thumbnail: { size: [100, 100], mode: outbound }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done! Really simple, doesn’t it?&lt;/p&gt;

&lt;p&gt;But, as you can see, we have the directive &lt;code&gt;cache&lt;/code&gt; in each of the two defined filters: these directives use the &lt;code&gt;CacheResolver&lt;/code&gt; called &lt;code&gt;cache_resolver_aws_s3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, as it may be yet clear, we have to define the &lt;code&gt;cache_resolver_aws_s3&lt;/code&gt;. We are fastly moving toward the STEP 3! &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Define the &lt;code&gt;CacheResolver&lt;/code&gt; &lt;code&gt;AwsS3Resolver&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;As we want to save our generated thumbnails to AWS S£, we need to use the &lt;code&gt;CacheResolver&lt;/code&gt; &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/doc/cache-resolver/aws_s3.rst"&gt;&lt;code&gt;AwsS3Resolver&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this case, too, we can decide to define our resolver as a service or using the factory.&lt;br&gt;&lt;br&gt;
In this case, too, I’ll use the factory, but you can choose to use the method you like.&lt;/p&gt;

&lt;p&gt;So, in the &lt;code&gt;config.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;liip_imagine:
    loaders:
        ...
    resolvers:
       cache_resolver_aws_s3:
          aws_s3:
              client_config:
                  credentials:
                      key: %amazon.s3.key%
                      secret: %amazon.s3.secret%
                  region: %amazon.s3.region%
                  version: %amazon.s3.version%
              get_options:
                  Scheme: 'https'
              put_options:
                  CacheControl: 'max-age=86400'
    filter_sets:
        ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test that all works well
&lt;/h2&gt;

&lt;p&gt;And now that we have all configured, it’s time to test!&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit &lt;code&gt;DefaultController::TestThumbAction&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;First: change the code into the action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    /**
     * @Route("/test_thumb", name="thumb")
     * @Template()
     */
    public function testThumbAction()
    {
        //$filesystem = $this-&amp;gt;get('knp_gaufrette.filesystem_map')-&amp;gt;get('filesystem_aws_s3_images');
        //$file = $filesystem-&amp;gt;get('symfony2.png');

        /*
         * Note the use of the dump() function.
         * If you don't have the VarDumperComponent installed, use var_dump().
         * @see http://symfony.com/doc/current/components/var_dumper/introduction.html
         */
        //dump($file);die;

        /*$image = 'https://s3-'.
            $this-&amp;gt;getParameter('amazon.s3.region').
            '.amazonaws.com/'.
            $this-&amp;gt;getParameter('amazon.s3.bucket').
            '/images/symfony2.png';*/

        $image = 'symfony2.png';

        return [
            'image' =&amp;gt; $image
        ];
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Activate filters in the template
&lt;/h3&gt;

&lt;p&gt;Now activate the filters in the template removing comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_small_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;img src="{{ image | imagine_filter('test_small_thumb') }}" alt="" /&amp;gt;

&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_medium_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;img src="{{ image | imagine_filter('test_medium_thumb') }}" alt="" /&amp;gt;

&amp;lt;h1&amp;gt;Original Image&amp;lt;/h1&amp;gt;
&amp;lt;img src="{{ image }}" alt="" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test!
&lt;/h3&gt;

&lt;p&gt;Reload your page at &lt;code&gt;http://127.0.0.1:8000/test_thumb&lt;/code&gt; and, if you’ve done well all the steps, you’ll see something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--44jO98CM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2016/01/590-screenshot-liip_imagine_bundle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--44jO98CM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2016/01/590-screenshot-liip_imagine_bundle.png" alt="590-screenshot-liip_imagine_bundle" width="553" height="827"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Obviously, “Original image” is no longer visible as the &lt;code&gt;path&lt;/code&gt; isn’t correct (we commented it in the controller!).&lt;/p&gt;

&lt;p&gt;Done: now you have &lt;code&gt;LiipImagineBundle&lt;/code&gt; up and running. You can now save your images to Amazon AWS S3, get them and resize to thumbnail sizes and save them again on AWS S3! &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VKotEYmA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://s.w.org/images/core/emoji/14.0.0/72x72/1f642.png" alt="🙂" width="72" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Some things to note: what happened behind the scenes
&lt;/h2&gt;

&lt;p&gt;The first thing you should note is the URLs of the thumbnails.&lt;/p&gt;

&lt;p&gt;The first time you load the page the resulting URLs are those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_small_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;img src="http://127.0.0.1:8000/media/cache/resolve/test_small_thumb/symfony2.png" alt="" /&amp;gt;

&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_medium_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;img src="http://127.0.0.1:8000/media/cache/resolve/test_medium_thumb/symfony2.png" alt="" /&amp;gt;

&amp;lt;h1&amp;gt;Original Image&amp;lt;/h1&amp;gt;
&amp;lt;img src="symfony2.png" alt="" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can note, they are in this form: &lt;code&gt;media/cache/resolve/[filter_name]/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Try to reload the page.&lt;/p&gt;

&lt;p&gt;The resulting URLs, this time, are those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_small_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;img src="https://s3-eu-west-1.amazonaws.com/trustback-me-dev/test_small_thumb/symfony2.png" alt="" /&amp;gt;

&amp;lt;h1&amp;gt;Test resized with &amp;lt;code&amp;gt;test_medium_thumb&amp;lt;/code&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;img src="https://s3-eu-west-1.amazonaws.com/trustback-me-dev/test_medium_thumb/symfony2.png" alt="" /&amp;gt;

&amp;lt;h1&amp;gt;Original Image&amp;lt;/h1&amp;gt;
&amp;lt;img src="symfony2.png" alt="" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They refers to the public URL on AWS S3: &lt;code&gt;https://s3-eu-west-1.amazonaws.com/[bucket_name]/[filter_name/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In fact, the first time you load the images, if &lt;code&gt;LiipImagineBundle&lt;/code&gt; cannot find a cached version, points to a controller defined in &lt;a href="https://github.com/liip/LiipImagineBundle/blob/master/Resources/config/routing.xml"&gt;&lt;code&gt;vendor/liip/imagine-bundle/Resources/config/routing.xml&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The URL you see the first time points to the controller that applies the filters. Once the resulting image is cached, the next time &lt;code&gt;LiipImagineBundle&lt;/code&gt; uses directly the URL of the bucket on AWS S3.&lt;/p&gt;

&lt;p&gt;The second thing you should note, is that &lt;code&gt;LiipImagineBundle&lt;/code&gt; automatically creates a folder for each filter in your bucket:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--f5IHt1Pk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2016/01/590-screenshot-liip_imagine_bundle-bucket-details.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--f5IHt1Pk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://io.serendipityhq.com/wp-content/uploads/sites/2/2016/01/590-screenshot-liip_imagine_bundle-bucket-details.png" alt="590-screenshot-liip_imagine_bundle-bucket-details" width="688" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, for each configured filter, the resulting images are saved into a dedicated folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;And finally, some advices to debug in case of issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep attention to differences between UPPERCASE and lowercase letters: maybe you can write &lt;code&gt;filesystem_aws_s3_images&lt;/code&gt; in one place and &lt;code&gt;filesystem_aws_S3_images&lt;/code&gt; in another place. Spot the difference!&lt;/li&gt;
&lt;li&gt;As the bundle is usually usedfrom inside a template, you’ll don’t get exceptions or errors. If something doesn’t work, go to the logs! There there are all the errors: find them looking for strings like “gaufrette” or “liip_imagine” or “liip/imagine-bundle” or “request.ERROR” or “php.DEBUG” and you’ll find all the info you need to understand what went wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And finally, before to close this long post, remember that in the &lt;a href="https://github.com/liip/LiipImagineBundle/issues/"&gt;Issues section&lt;/a&gt; on GitHub there are a lot of useful information to better understand how to use the bundle.&lt;/p&gt;

&lt;p&gt;Here are a couple of interesting issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/liip/LiipImagineBundle/issues/203"&gt;Complete S3 Setup: Original Images and cached Images on AWS S3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liip/LiipImagineBundle/issues/335"&gt;need https option for aws stream&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember to “Make. Ideas. Happen.”.&lt;/p&gt;

&lt;p&gt;I wish you flocking users, see you soon!&lt;/p&gt;

&lt;p&gt;L'articolo &lt;a href="https://io.serendipityhq.com/experience/how-to-use-liipimaginebundle-to-manage-thumbnails-through-amazon-s3/"&gt;How to use LiipImagineBundle to manage thumbnails through Amazon S3&lt;/a&gt; proviene da &lt;a href="https://io.serendipityhq.com"&gt;ÐΞV Experiences by Serendipity HQ&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>amazonwebservices</category>
      <category>php</category>
      <category>symfony</category>
    </item>
  </channel>
</rss>
