<?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>Forem: pretzelhands</title>
    <description>The latest articles on Forem by pretzelhands (@pretzelhands).</description>
    <link>https://forem.com/pretzelhands</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%2F126697%2F3a2f08d9-b927-4125-b64c-9df79d8b6d61.png</url>
      <title>Forem: pretzelhands</title>
      <link>https://forem.com/pretzelhands</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/pretzelhands"/>
    <language>en</language>
    <item>
      <title>Build your own PSR-4 autoloader</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Tue, 26 Jan 2021 09:51:14 +0000</pubDate>
      <link>https://forem.com/pretzelhands/build-your-own-psr-4-autoloader-2b56</link>
      <guid>https://forem.com/pretzelhands/build-your-own-psr-4-autoloader-2b56</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;You can find the repository for this project &lt;a href="https://github.com/pretzelhands/psr-4"&gt;on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're anything like me, you've probably wondered before: "What kind of magic makes Composer go?" - You look at the source code of &lt;code&gt;vendor/autoload.php&lt;/code&gt; and see it links to yet another autoloader that's called &lt;code&gt;autoload_real.php&lt;/code&gt; Then there's a bunch of hashes and it's confusing and eventually you just accept that Composer is magic and you aren't worthy of its secrets.&lt;/p&gt;

&lt;p&gt;Today, let's look at how you can build your own Composer. At least the autoloading part. We won't talk about the seven million other features that Composer provides to us aside from that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the project
&lt;/h2&gt;

&lt;p&gt;This time, all you need to bring is any kind of PHP installation. Technically it needs to be at least PHP 5.3, but I very much hope that you aren't running anything that old.&lt;/p&gt;

&lt;p&gt;As always, we'll start with what we want our project structure to look like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psr-4/
├─ autoloader.php
├─ app.php
├─ src/
│  ├─ Pretzel/
│  │  ├─ RandomClass.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;autoloader.php&lt;/code&gt; is going to be what it says. This file will be roughly equivalent to &lt;code&gt;vendor/autoload.php&lt;/code&gt; when using Composer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app.php&lt;/code&gt; is going to be our test file where we include the autoloader and try to use our classes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/&lt;/code&gt; will contain our application-specific code. The folder doesn't have to be Pretzel. You pick whatever makes you happy.&lt;/p&gt;

&lt;p&gt;Our stated goal is to load every class that is located in &lt;code&gt;src/Pretzel&lt;/code&gt;. Each subdirectory constitutes a new namespace. So any file in &lt;code&gt;src/Pretzel/Factory&lt;/code&gt; has a namespace of &lt;code&gt;Pretzel\Factory&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The magic spice: spl_autoload_register
&lt;/h2&gt;

&lt;p&gt;All we really need to create an autoloader is that function: &lt;code&gt;spl_autoload_register&lt;/code&gt;. You can read more details about it in &lt;a href="https://www.php.net/manual/en/function.spl-autoload-register.php"&gt;the PHP documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You call it like this&lt;br&gt;
&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="nb"&gt;spl_autoload_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Perform unknowable magic here&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All you need to pass in is a &lt;a href="https://www.php.net/manual/en/language.types.callable.php"&gt;callable&lt;/a&gt; of some kind that receives one argument: a string with a class name. You can register multiple of these callbacks too. This is useful if you want to have a few logically different ways to load classes.&lt;/p&gt;

&lt;p&gt;After your function is registered, PHP will call it every time a class is used for the first time. This is usually either when you call &lt;code&gt;new&lt;/code&gt; on it, or when you use a static function on it. Let's see that in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interacting with spl_autoload_register
&lt;/h2&gt;

&lt;p&gt;I'll set up my &lt;code&gt;autoloader.php&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="nb"&gt;spl_autoload_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&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;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&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;



&lt;p&gt;For now we'll just print the class name that was desired and exit. We also need a class to load, so let's build a quick &lt;code&gt;PretzelFactory&lt;/code&gt;. It returns a pretzel emoji from a static method. It's very fancy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// src/Pretzel/Factory/PretzelFactory

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

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Pretzel\Factory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PretzelFactory&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;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getPretzel&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="k"&gt;return&lt;/span&gt; &lt;span class="s1"&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;



&lt;p&gt;We need to use that class somewhere. This is what our &lt;code&gt;app.php&lt;/code&gt; file is for. We'll have to include our autoloader, pull the class in and then call the static method. Something like this.&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/autoloader.php'&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;Pretzel\Factory\PretzelFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nc"&gt;PretzelFactory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getPretzel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we run our &lt;code&gt;app.php&lt;/code&gt; file, this is the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php app.php
Pretzel&lt;span class="se"&gt;\F&lt;/span&gt;actory&lt;span class="se"&gt;\P&lt;/span&gt;retzelFactory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we get the fully-qualified class name (&lt;code&gt;FQCN&lt;/code&gt;) passed to us. Now &lt;a href="https://www.php-fig.org/psr/psr-4/"&gt;PSR-4&lt;/a&gt; states that a FQCN name consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A namespace prefix (e.g. &lt;code&gt;Pretzel&lt;/code&gt;) that corresponds to a base directory (e.g. &lt;code&gt;src/Pretzel&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Zero or more sub-namespaces (e.g. &lt;code&gt;Factory&lt;/code&gt;) that correspond to a directory (e.g. &lt;code&gt;src/Pretzel/Factory&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;One class name corresponding to a PHP file with that same name (e.g. &lt;code&gt;PretzelFactory.php&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of you might have realized by now, that our FQCN is essentially a direct path to the file we need to load. How convenient!&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading the appropriate files
&lt;/h2&gt;

&lt;p&gt;To load our file, first we need to turn our FQCN into an actual path. This can be done by replacing every backslash with a forward slash and appending &lt;code&gt;.php&lt;/code&gt; at the end. With that, we can already autoload files from a hard-coded base path!&lt;/p&gt;

&lt;p&gt;Let's try it out!&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fqcnToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$fqcn&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="nb"&gt;str_replace&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="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$fqcn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;spl_autoload_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fqcnToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/src/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$path&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;



&lt;p&gt;After adding this, we can just run our &lt;code&gt;app.php&lt;/code&gt; script again&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php app.php
🥨
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great success! This shows you what autoloading boils down to: A fancy way to require a PHP file just in time when it's needed. But if we want to be (reasonably) spec-compliant, we still have work to do.&lt;/p&gt;

&lt;p&gt;Right now the path we load from is hard-coded to be &lt;code&gt;src/&lt;/code&gt; but according to PSR-4, every package is free to define its own namespace prefix that maps to a path. This is essentially what you do every time you set up autoloading in Composer&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"autoload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"psr-4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Pretzel\\"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/Pretzel"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So let's try to parse this JSON snippet and use it to load our classes. I'll call it &lt;code&gt;conductor.json&lt;/code&gt;, because that's what the logo of Composer depicts, and we're basically a cheap knock-off. 🤪&lt;/p&gt;

&lt;h2&gt;
  
  
  Autoloading according to configuration
&lt;/h2&gt;

&lt;p&gt;The first thing we have to do is to grab our JSON file and turn it into something we can actually work with (i.e. an associative array). Luckily, PHP has native functions for both of those things.&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="nv"&gt;$configuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'conductor.json'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$namespaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'autoload'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'psr-4'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// .. SNIP ..&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the user is free to specify as many namespaces and base directories as she wants, so we need to check if the prefixed/first namespace is available in our &lt;code&gt;psr-4&lt;/code&gt; associative array. We could use a &lt;code&gt;foreach&lt;/code&gt; loop, but we can also make it a bit more efficient using a function called &lt;code&gt;strtok()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="nb"&gt;spl_autoload_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&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="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'\\'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// We don't handle that namespace.&lt;/span&gt;
    &lt;span class="c1"&gt;// Return and hope some other autoloader handles it.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$namespaces&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="nv"&gt;$baseDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$prefix&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;



&lt;p&gt;What &lt;code&gt;strtok&lt;/code&gt; does is tokenize the string according to a separator. If we pass in the backslash, it will return us everything up to the first backslash, excluding that. To correctly match our configuration, we just add it again ourselves.&lt;/p&gt;

&lt;p&gt;For the performance nerds among you this makes sure that we find the proper base directory in &lt;code&gt;O(1)&lt;/code&gt; time.&lt;/p&gt;

&lt;p&gt;Then we check if this namespace prefix maps to a path in our configuration. If it does, great! If it doesn't, we just return from the function and hope that someone else handles this case. We can't do anything else with it.&lt;/p&gt;

&lt;p&gt;The next step is to remove the prefixed namespace from our class name. This is because we know exactly what directory the prefix maps to, and we want to start building our final path from that. We can do this in our &lt;code&gt;fqcnToPath&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fqcnToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$fqcn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$relativeClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ltrim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fqcn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str_replace&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="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$relativeClass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;spl_autoload_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&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="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'\\'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// We don't handle that namespace.&lt;/span&gt;
    &lt;span class="c1"&gt;// Return and hope some other autoloader handles it.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$namespaces&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="nv"&gt;$baseDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fqcnToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$prefix&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;



&lt;p&gt;Once again PHP has our back! We use &lt;code&gt;ltrim&lt;/code&gt; to remove the main namespace from the beginning of our FQCN. This gives us the "relative" class, in the same sense as having a relative path. The rest of the function stays the same.&lt;/p&gt;

&lt;p&gt;At this point we have a base directory and a relative path going out from that base directory. So the final piece that's missing is trying to require the appropriate file!&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="nb"&gt;spl_autoload_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&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="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'\\'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// We don't handle that namespace.&lt;/span&gt;
    &lt;span class="c1"&gt;// Return and hope some other autoloader handles it.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$namespaces&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="nv"&gt;$baseDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fqcnToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="nv"&gt;$baseDirectory&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$path&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;



&lt;p&gt;At this point we can run our &lt;code&gt;app.php&lt;/code&gt; again and see if it works as advertised.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php app.php
🥨
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! So now we can add PSR-4 autoloading configuration to our JSON file just as if it were Composer. Of course, one should probably add some more input validation to this code. Users could forget to specify a &lt;code&gt;\&lt;/code&gt; in their namespace. Or they could accidentally add a &lt;code&gt;/&lt;/code&gt; in the base directory. But I'll leave that part to you, dear reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;While this is a simplified example of what Composer does internally, it still shows you the general principles of PSR-4 autoloading and what goes into it. Once you break it down, it's really not as complex as it seems!&lt;/p&gt;

&lt;p&gt;The main reason I even got the idea was due to a side project I'm silently hacking away on. I needed a simple way to load in PHP classes that define plugins, and well, here we are.&lt;/p&gt;

&lt;p&gt;As always, the entire code for this blog post is &lt;a href="https://github.com/pretzelhands/psr-4"&gt;available on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;Cover photo by &lt;a href="https://unsplash.com/@d3cima?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Henri L.&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/php-code?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building an easily distributable single-file command line app with PHP and Composer</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Thu, 21 Jan 2021 13:59:04 +0000</pubDate>
      <link>https://forem.com/pretzelhands/building-an-easily-distributable-single-file-command-line-app-with-php-and-composer-2hfi</link>
      <guid>https://forem.com/pretzelhands/building-an-easily-distributable-single-file-command-line-app-with-php-and-composer-2hfi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post was compiled from &lt;a href="https://twitter.com/pretzelhds/status/1351851922744029185" rel="noopener noreferrer"&gt;two of my&lt;/a&gt; &lt;a href="https://twitter.com/pretzelhds/status/1352217411437604865" rel="noopener noreferrer"&gt;Twitter threads&lt;/a&gt;. Be sure to &lt;a href="https://twitter.com/pretzelhds" rel="noopener noreferrer"&gt;follow me&lt;/a&gt; to see the new ones first!&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In May 2020, Symfony released version 5.1 of their components and included something truly beautiful for us developers: The &lt;code&gt;SingleCommandApplication&lt;/code&gt; class. This allows you to define and immediately run a command line app in 32 lines of code. Here it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;#!/usr/bin/env php

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

&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload.php'&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\InputInterface&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\Console\Output\OutputInterface&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\SingleCommandApplication&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;JOKE_API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://official-joke-api.appspot.com/jokes/%s/random'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;InputInterface&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;OutputInterface&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$jokeResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;JOKE_API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$joke&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jokeResponse&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="nv"&gt;$joke&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;setup&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="s2"&gt;"&amp;lt;info&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$joke&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;punchline&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/info&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&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;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SingleCommandApplication&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Joke Fetcher'&lt;/span&gt;&lt;span class="p"&gt;)&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="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'t'&lt;/span&gt;&lt;span class="p"&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_OPTIONAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'Get a joke relating to a specific topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'general'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCode&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And this is what it looks like when you run it. &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%2Fi%2F8lks7w5dof7q3mu29c6u.jpg" 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%2Fi%2F8lks7w5dof7q3mu29c6u.jpg" alt="Command line app fetching two jokes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's examine the app piece by piece.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dependencies
&lt;/h2&gt;

&lt;p&gt;For building the command line app, we require only a single dependency from Symfony.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;composer require symfony/console
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once that's installed, you're ready to go!&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting the stage
&lt;/h2&gt;

&lt;p&gt;The first 12 lines are all setup. We include the autoloader and all classes we need. We also setup our endpoint as a constant so it doesn't become a magic string floating around in our application code.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;#!/usr/bin/env php

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

&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload.php'&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\InputInterface&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\Console\Output\OutputInterface&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\SingleCommandApplication&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;JOKE_API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://official-joke-api.appspot.com/jokes/%s/random'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;👀 Take note of the shebang (&lt;code&gt;#!/usr/bin/env php&lt;/code&gt;) on line 1! It enables us to run our helper using &lt;code&gt;./joke-fetcher&lt;/code&gt; instead of having to call PHP specifically (&lt;code&gt;php joke-fetcher&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Defining the command
&lt;/h2&gt;

&lt;p&gt;Our actual command is just a PHP function called exactly that: &lt;code&gt;command&lt;/code&gt;. Making it a separate function is a style choice. You can pass a closure directly to the &lt;code&gt;SingleCommandApplication&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;InputInterface&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;OutputInterface&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$jokeResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;JOKE_API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$topic&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$joke&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jokeResponse&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="nv"&gt;$joke&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;setup&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="s2"&gt;"&amp;lt;info&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$joke&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;punchline&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/info&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In here we fetch the topic that the user can give us, and request our joke endpoint. We then output it with some pretty formatting.&lt;/p&gt;

&lt;p&gt;In a real application, you'd want to use a proper HTTP client to do this. &lt;code&gt;file_get_contents&lt;/code&gt;/&lt;code&gt;json_decode&lt;/code&gt; is just easy for demonstration.&lt;/p&gt;

&lt;p&gt;Also you can also learn more about console output formatting in &lt;a href="https://symfony.com/doc/current/console/coloring.html#using-color-styles" rel="noopener noreferrer"&gt;the Symfony docs&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Building our &lt;code&gt;SingleCommandApplication&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the interesting part! We create a &lt;code&gt;SingleCommandApplication&lt;/code&gt; and immediately use the return value to define various settings.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SingleCommandApplication&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Joke Fetcher'&lt;/span&gt;&lt;span class="p"&gt;)&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="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'t'&lt;/span&gt;&lt;span class="p"&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_OPTIONAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'Get a joke relating to a specific topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'general'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCode&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;setName&lt;/code&gt; is optional. It just improves the output of some pre-delivered options (e.g. &lt;code&gt;--version&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;addOption&lt;/code&gt; allows us to add a command line flag the user can set. It takes the following arguments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The name of the option (&lt;code&gt;--topic&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A shortcut for the option (&lt;code&gt;-t&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A "mode" (Is the option is required, optional or boolean?)&lt;/li&gt;
&lt;li&gt;A description of the command (used for &lt;code&gt;--help&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A default value (The standard topic of our jokes)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&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="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'t'&lt;/span&gt;&lt;span class="p"&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_OPTIONAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Get a joke relating to a specific topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'general'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you're using PHP8 already: This is a great place to use named arguments to selectively set options. &lt;/p&gt;

&lt;p&gt;However, the optional arguments all have UX implications. It's in your best interest to set sensible values for them.&lt;/p&gt;

&lt;p&gt;The second to last line (&lt;code&gt;setCode&lt;/code&gt;) sets the code the command line app should run. This can be any kind of &lt;a href="https://t.co/FhBPOmZ0LV" rel="noopener noreferrer"&gt;PHP callable&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁‍♂️ I like pulling out functions to avoid nesting and improve reusability&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last line (&lt;code&gt;-&amp;gt;run()&lt;/code&gt;) immediately runs the app we've just made.&lt;/p&gt;

&lt;p&gt;And there you have it. You can now run your app and enjoy corny jokes!&lt;/p&gt;

&lt;p&gt;If you want, you can also rename your PHP file to remove the &lt;code&gt;.php&lt;/code&gt; extension and run it as &lt;code&gt;./&amp;lt;filename&amp;gt;&lt;/code&gt; -- You just have to add the executable flag with &lt;code&gt;chmod&lt;/code&gt;!&lt;/p&gt;
&lt;h2&gt;
  
  
  Making the app distributable
&lt;/h2&gt;

&lt;p&gt;To make our app easily distributable and executable, we'll have to turn it into a PHP archive, better known as a PHAR file.&lt;/p&gt;

&lt;p&gt;For this we'll use the ✨ beautiful &lt;a href="https://github.com/clue/phar-composer" rel="noopener noreferrer"&gt;&lt;code&gt;phar-composer&lt;/code&gt;&lt;/a&gt; package by &lt;a href="https://clue.engineering" rel="noopener noreferrer"&gt;clue.engineering&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installing it is a lot like installing composer, if you do it manually. The steps look like this&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-JOL&lt;/span&gt; https://clue.engineering/phar-composer-latest.phar&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;php phar-composer-1.2.0.phar &lt;span class="nb"&gt;install &lt;/span&gt;clue/phar-composer
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;rm &lt;/span&gt;phar-composer-1.2.0.phar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;❗️ You should always double-check what you're pulling into your shell with &lt;code&gt;curl/wget&lt;/code&gt;, but I vouch for this one&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also note, that the version may not be 1.2.0 for you. The package is regularly updated.&lt;/p&gt;
&lt;h2&gt;
  
  
  Preparing our composer.json
&lt;/h2&gt;

&lt;p&gt;Now we need to add a &lt;code&gt;bin&lt;/code&gt; property to our &lt;code&gt;composer.json&lt;/code&gt; to let &lt;code&gt;phar-composer&lt;/code&gt; know which file is runnable. In my case it's &lt;code&gt;cli.php&lt;/code&gt; and it's in the app root folder&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pretzelhands/joke-fetcher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"symfony/console"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.2"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cli.php"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;As a ✨quality-of-life improvement✨ I also like to add a Composer script called &lt;code&gt;phar&lt;/code&gt; that just calls the necessary command for us.&lt;/p&gt;

&lt;p&gt;So instead of running&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;phar-composer build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I can run&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;composer run phar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;which is more consistent within most of my projects. To add the Composer script, edit your &lt;code&gt;composer.json&lt;/code&gt; file and add a new &lt;code&gt;scripts&lt;/code&gt; property like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pretzelhands/joke-fetcher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"symfony/console"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.2"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"phar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phar-composer build"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cli.php"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now you can run it! In my case the &lt;code&gt;joke-fetcher&lt;/code&gt; app we wrote above comes out to ~980KB.&lt;/p&gt;

&lt;p&gt;💄 For aesthetic reasons you can also rename your PHAR to remove the file extension.&lt;/p&gt;

&lt;p&gt;🔥 You can share this app with anyone who has PHP installed and they can just run it with chmod 755!~&lt;/p&gt;
&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🤬 &lt;code&gt;phar-composer&lt;/code&gt; will complain if &lt;code&gt;phar.readonly&lt;/code&gt; is set to &lt;code&gt;off&lt;/code&gt; in your php.ini - This doesn't stop it from working. But it advises you how to get rid of the message&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💬 For the name of your PHAR file, &lt;code&gt;phar-composer&lt;/code&gt; looks at the &lt;code&gt;name&lt;/code&gt; prop in your &lt;code&gt;composer.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🚮 &lt;code&gt;phar-composer&lt;/code&gt; won't check if there's already a PHAR in the project directory. It'll just package it in there, making your PHAR twice as big&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need to delete it yourself. You can make this part of the &lt;code&gt;composer run phar&lt;/code&gt; script. This is left as an exercise for the reader ;)&lt;/p&gt;
&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;Look at these links if you want to learn more about the Symfony Console command, &lt;code&gt;phar-composer&lt;/code&gt; and various details of PHP!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;code&gt;phar-composer&lt;/code&gt;: &lt;a href="https://github.com/clue/phar-composer" rel="noopener noreferrer"&gt;https://github.com/clue/phar-composer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;👀 PHAR files: &lt;a href="https://php.net/manual/en/intro.phar.php" rel="noopener noreferrer"&gt;https://php.net/manual/en/intro.phar.php&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SingleCommandApplication&lt;/code&gt;: &lt;a href="https://symfony.com/doc/current/components/console/single_command_tool.html" rel="noopener noreferrer"&gt;https://symfony.com/doc/current/components/console/single_command_tool.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Symfony Console component: &lt;a href="https://symfony.com/doc/current/components/console.html" rel="noopener noreferrer"&gt;https://symfony.com/doc/current/components/console.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Console options and arguments: &lt;a href="https://symfony.com/doc/current/components/console/console_arguments.html" rel="noopener noreferrer"&gt;https://symfony.com/doc/current/components/console/console_arguments.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here's a gist, for anyone who wants to see the entire code at once!&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;






&lt;blockquote&gt;
&lt;p&gt;If you enjoyed this post and want to read more like this, &lt;a href="https://twitter.com/pretzelhds" rel="noopener noreferrer"&gt;follow me&lt;/a&gt; on Twitter! Or &lt;a href="https://pretzelhands.com/newsletter" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; to my newsletter&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>php</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>automation</category>
    </item>
    <item>
      <title>Scratching my own itch: Building Notebag</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Sat, 02 May 2020 20:08:03 +0000</pubDate>
      <link>https://forem.com/pretzelhands/scratching-my-own-itch-building-notebag-42co</link>
      <guid>https://forem.com/pretzelhands/scratching-my-own-itch-building-notebag-42co</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Notebag is on &lt;a href="https://producthunt.com/posts/notebag-2"&gt;Product Hunt!&lt;/a&gt; Check it out!&lt;/p&gt;

&lt;p&gt;This post was originally published on &lt;a href="https://pretzelhands.com/posts/scratching-the-itch"&gt;my personal blog&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was never any good at coming up with ideas. I'm not sure whether you could attribute it to a lack of creativity, my life being "boring" by many peoples standards or whether I've just been satisfied with existing solutions to all of my discomforts and problems.&lt;/p&gt;

&lt;p&gt;Well, except for that one thing where I never found a satisfying solution: Note taking. I tried a few different approaches. For a while I would put my notes into my IDEs scratch file feature. That wasn't exactly a very scalable or searchable solution, but it was good enough for storing code snippets in. &lt;/p&gt;

&lt;p&gt;For another while I had my writings in Apple Notes. I liked the simplicity of it, but it didn't have support for typing out notes in Markdown, which made formatting cumbersome. &lt;/p&gt;

&lt;p&gt;Then I tried note taking wunderkind Bear, which is beloved by many. And for a while I was very happy with it. It has a wonderful set of features and &lt;em&gt;great&lt;/em&gt; user experience. Seriously, hats off to the people at Bear. But I still had to reach for my mouse to do things every now and then. And there was still the thing where I had to either open the app on demand or switch around all my open windows with &lt;code&gt;Cmd-Tab&lt;/code&gt;. It still felt off.&lt;/p&gt;

&lt;p&gt;This entire journey went on for a good year or so. I had fought off the idea of writing a note taking app because I felt&lt;br&gt;
that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A) It would be super complicated&lt;/li&gt;
&lt;li&gt;B) The market for note taking apps is incredibly over saturated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I still believe that the second point holds true to a certain extent. There is certainly a lot of competition out there. But the first point was shattered when I found out about a lovely little thing called &lt;a href="https://tiptap.scrumpy.io/"&gt;tiptap&lt;/a&gt;. It's based on ProseMirror which is a wonderful library for building rich text editors.&lt;/p&gt;

&lt;p&gt;And on &lt;a href="https://twitter.com/pretzelhds/status/1236402270373318667"&gt;March 7, 2020&lt;/a&gt; I finally got fed up enough to start writing code. (Although there were &lt;a href="https://twitter.com/pretzelhds/status/1226198239289978880"&gt;earlier&lt;/a&gt; &lt;a href="https://twitter.com/pretzelhds/status/1235257841088368640"&gt;indicators&lt;/a&gt; of what was about to happen).&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://notebag.app"&gt;Notebag&lt;/a&gt; is an Electron-based app since I am primarily a web developer and native code seems scary. I would have loved&lt;br&gt;
to give Swift a try, but in the end stuck to what I know best. For the frontend I landed on Vue since that is what Tiptap uses and I don't really hold much of a strong opinion in the great war of the frontend frameworks.&lt;/p&gt;

&lt;p&gt;One great thing that allowed development to move quickly is Prosemirrors concept of input rules. These are essentially little functions that take a regex you want to search for and then spit out appropriate HTML for the thing you just entered. This is what powers all of the Markdown formatting as well as a few custom functionalities such as the nestable categories&lt;br&gt;
and Zettelkasten-type links.&lt;/p&gt;

&lt;p&gt;For most of March I was still working on this very sporadically as I was pre-occupied with contracting work for most of my week. Sadly due to the world going a bit haywire I ended up having not so much contract work anymore by April at which point I started focusing on the development in earnest.&lt;/p&gt;

&lt;p&gt;Within a week or so I had implemented most of the barebones features of a note taking app (funnily enough, switching between notes was one of the last ones. Oops!) And I sent out my first beta builds. At this point the app &lt;a href="https://twitter.com/pretzelhds/status/1246074902941057024"&gt;looked a bit like a boring Apple Notes clone.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is what I sent out to a few trusted beta testers&lt;/p&gt;

&lt;h2&gt;
  
  
  Differentiating the app
&lt;/h2&gt;

&lt;p&gt;This was also the time at which I started drilling down on what I consider the unique selling points of the app. At first there is the omnibar or "Go To Anything". This is essentially the same thing as in any modern IDE or code editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YqVUCK_Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/media/EVROgAKWkAAIvT8%3Fformat%3Djpg%26name%3Dlarge" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YqVUCK_Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/media/EVROgAKWkAAIvT8%3Fformat%3Djpg%26name%3Dlarge" alt="Omnibar"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You have a full fuzzy search for your notes. And this is also where the keyboard focus really took overhand. I spent a good chunk of the next two weeks assigning &lt;code&gt;tabindex&lt;/code&gt;es to every important element, writing countless &lt;code&gt;:hover, :active, :focus&lt;/code&gt; styles and adding numerous keyboard bindings that you can use to get around the app.&lt;/p&gt;

&lt;p&gt;There was lots of talking with beta testers and refining and implementing and polishing. You never expect how much there is to do until you start writing it down and ending up with a todo list in the mid double digits.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I even marketing
&lt;/h2&gt;

&lt;p&gt;Once the app reached a reasonably stable level of maturity, I had to get around to the point I am the worst at. The one I had pushed away again and again for as long as I could: I had to build &lt;a href="https://notebag.app"&gt;a landing page&lt;/a&gt; to market &lt;br&gt;
this thing. Oh dear&lt;/p&gt;

&lt;p&gt;The struggles started with naming. In total I went through 27 names before the right one appeared. Some of them were: Keynote, Typemark, Markflow, Swiftnote, Feathermark, Keynib, and Crosskey. The final name wasn't even my idea. The credit for it goes to &lt;a href="https://twitter.com/h0h0h0"&gt;Shaun Farrugia&lt;/a&gt;. But I liked it and it stuck.&lt;/p&gt;

&lt;p&gt;Next came logo design. I've always had a certain love for RPGs and I liked the way the old timey bags full of gold looked. It's not a perfect fit for the app at hand, but the design came together quickly and I was quite pleased.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6wUjzpu2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://notebag.app/assets/logo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6wUjzpu2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://notebag.app/assets/logo.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And finally the landing page. I grabbed myself a bit of Tailwind and Jekyll and got to work. For what its worth I still have no idea how marketing experts do it, but I discussed the copy with people, I polished the experience of reading it  for the first time, I made sure it looks decent on all the devices I have around and then it got pushed to live.&lt;/p&gt;

&lt;p&gt;Since going live it managed to convince three people to get the app, so I guess I'm not doing too bad!&lt;/p&gt;

&lt;h2&gt;
  
  
  Launching
&lt;/h2&gt;

&lt;p&gt;And here we stand, shortly before the big launch. This is really the first time I have built a proper product of my own and I am excited and also terrified of where the journey goes next. Here's hoping that the name "Notebag" can eventually stand proudly among the big stars of note taking.&lt;/p&gt;

&lt;p&gt;Look for the launch on ProductHunt on Monday, May 4! And in the meantime, feel free to check out Notebag at &lt;a href="https://notebag.app"&gt;notebag.app!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>vue</category>
      <category>development</category>
    </item>
    <item>
      <title>A recap of 2019 and looking towards 2020</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Sat, 21 Dec 2019 14:15:57 +0000</pubDate>
      <link>https://forem.com/pretzelhands/a-recap-of-2019-and-looking-towards-2020-51ib</link>
      <guid>https://forem.com/pretzelhands/a-recap-of-2019-and-looking-towards-2020-51ib</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by Danil Aksenov on Unsplash&lt;/small&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href="https://pretzelhands.com/posts/recap-and-lookahead"&gt;my personal blog&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hey. It's been a while since I've written. It's almost been an entire year and what a year it's been. Since it's almost Christmas and things are quieting down for just a few days, let's take a look at what this year has been and where the next one will go.&lt;/p&gt;

&lt;h3&gt;
  
  
  Freelancing
&lt;/h3&gt;

&lt;p&gt;This year has been the most successful in terms of freelancing that I've ever had. Most of my year was still spent working with my long-term client, although that relationship is now ending due to legal reasons, that I don't want to type up here. The bottom line is that my contract runs out on Dec 31 of this year.&lt;/p&gt;

&lt;p&gt;Either way, it's been a fun time full of neat e-commerce projects and I gained a lot of domain knowledge about fintech and online payments that I'm sure will help me in some fashion in the future. If only because it gives me new insights the next time I have to integrate a payment gateway.&lt;/p&gt;

&lt;p&gt;After 1.5 years of working with a big enterprise-y client, I do however have to say that I'm looking forward to a more spontaneous/flexible work environment. When a company comes up on a few thousand employees, there will always be some obstacles and decisions naturally take longer. Also I will &lt;em&gt;not&lt;/em&gt; miss the meetings.&lt;/p&gt;

&lt;h4&gt;
  
  
  2020
&lt;/h4&gt;

&lt;p&gt;The new year will be exciting! For all of January I will be taking a month off. Not only to recharge and maybe pick up some things that I've dropped throughout the year, but also because I have to move. More on that below.&lt;/p&gt;

&lt;p&gt;Starting with February, I have found a new opportunity. It comes with decent pay and a flexible contigent of hours, while also being remote. Overall it sounds very great and I'm looking forward to it. Being remote also means I can cut out some time for going places every day. At 3h/day those are significant savings!&lt;/p&gt;

&lt;p&gt;And of course I'll stay in the whole e-commerce niche, since that seems to be a quite comfy spot from my current point of view!&lt;/p&gt;

&lt;h3&gt;
  
  
  Side projects
&lt;/h3&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;Can we talk about something else? No? Fine. This is the part of the year that got absolutely lost. I had grand plans of working on things in January, but between major life changes and above commuting problems I really had neither the time nor the energy to put into my side projects.&lt;/p&gt;

&lt;p&gt;They're all still there, lying in my &lt;code&gt;~/Projects&lt;/code&gt; folder, but most of them have been touched no later than February 2. This is quite painful as its the second time in a row that I've failed to achieve a side project goal. &lt;/p&gt;

&lt;p&gt;In 2018 I had set a goal of earning $50/mo from side projects. That didn't work out. In 2019 I didn't even set a goal and just got started working on things. Then I dropped the ball.&lt;/p&gt;

&lt;p&gt;Ouch.&lt;/p&gt;

&lt;h4&gt;
  
  
  2020
&lt;/h4&gt;

&lt;p&gt;I have no grand plans for 2020. I want to ship something, I want to make a product that I can be proud of and that I can call my own. Ideally, by the end of 2020 I want to somehow manage to get $1 into my pocket by the way of having a side project.&lt;/p&gt;

&lt;p&gt;Given my new contracting job and a non-existent commute, I think it should be possible. But we shall see. I've also been meaning to &lt;a href="https://dev.to/posts/jinx"&gt;rewrite my nginx wrapper&lt;/a&gt; in Python instead of Bash. Maybe that's where I can start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Life changes
&lt;/h3&gt;

&lt;p&gt;Ah yes, personal life changes. This is where things kind of exploded in terms of time spent and effort. But it all had an outcome that is very positive. So!&lt;/p&gt;

&lt;p&gt;In 2019 I got married to my girlfriend and now-wife of 4 years. She also moved here to be with me. This would've been quite the event on its own if not for the fact that we used to be a short jump of 10,000km (~6,200 miles) apart.&lt;/p&gt;

&lt;p&gt;Together with all of the visa restrictions that my country imposes and the fact that we had guests from her country coming by as well it turned out to be quite the adventure. Really, this is where the first half of my year just vanished.&lt;/p&gt;

&lt;p&gt;Either way, all things worked out well and we managed to make it happen!&lt;/p&gt;

&lt;h4&gt;
  
  
  2020
&lt;/h4&gt;

&lt;p&gt;As for the new year, we have a big move right at the start. I've always been a person who has lived in the rural countryside in small towns that were at best about 5,000 people big. This will change as we're moving smack dab into the middle of the second-largest city of Austria.&lt;/p&gt;

&lt;p&gt;Packing all of our stuff is not so fun, but the prospect of the move is very exciting! It'll be a totally new experience for me to live in a decently-sized city along with all of the benefits and drawbacks that might bring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing
&lt;/h3&gt;

&lt;p&gt;I'm still a bit blurry about what my 2020 will actually look like with a new place and a new job, but I'm also very excited about the possibilites that will bring.&lt;/p&gt;

&lt;p&gt;Here's hoping that your year was a great one and that the next one will bring you lots of good luck!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>life</category>
      <category>career</category>
    </item>
    <item>
      <title>Managing WordPress from your terminal</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Tue, 29 Jan 2019 21:08:07 +0000</pubDate>
      <link>https://forem.com/pretzelhands/managing-wordpress-from-your-terminal-55ha</link>
      <guid>https://forem.com/pretzelhands/managing-wordpress-from-your-terminal-55ha</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by Luca Bravo on Unsplash&lt;/small&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href="https://pretzelhands.com/posts/managing-wordpress-from-your-terminal"&gt;my personal blog&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm Richard. I'm addicted to WordPress.&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;Oh, this isn't the support group? Fine. But nevertheless, let me tell you about a humble command-line tool called &lt;code&gt;wp&lt;/code&gt; (or WP CLI). It is by far my favorite way to manage WordPress installations and saves me from having to click through the Dashboard approximately 95% of the time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Getting &lt;code&gt;wp&lt;/code&gt; installed isn't much of a hassle. If you already have &lt;a href="https://getcomposer.org"&gt;Composer&lt;/a&gt; installed it's a breeze. You need only require the package and you're done.&lt;/p&gt;

&lt;p&gt;I usually have &lt;code&gt;wp&lt;/code&gt; installed globally because I manage a lot of WordPress sites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer global require wp-cli/wp-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But of course you can also just require &lt;code&gt;wp&lt;/code&gt; for a project locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require wp-cli/wp-cli &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have Composer installed, there's also some &lt;a href="https://make.wordpress.org/cli/handbook/installing/"&gt;alternative installation methods.&lt;/a&gt; Feel free to pick the one that best suits your workflow.&lt;/p&gt;

&lt;p&gt;✨ Congratulations! Your life just became a whole lot more efficient!&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing WordPress from the command-line
&lt;/h3&gt;

&lt;p&gt;Now to see just what you can do with &lt;code&gt;wp&lt;/code&gt;, let's install WordPress and configure it. All done from the cozy comfort of your shell. The very first thing we need to do is to download the latest version of WordPress.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wp core download

Downloading WordPress x.y.z &lt;span class="o"&gt;(&lt;/span&gt;en_US&lt;span class="o"&gt;)&lt;/span&gt;...
md5 &lt;span class="nb"&gt;hash &lt;/span&gt;verified: 83bec78836aabac08f769d50f1bffe5d
Success: WordPress downloaded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That wasn't hard at all, was it? Now let's setup our &lt;code&gt;wp-config.php&lt;/code&gt; with all the necessary information. For this you can run the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wp config create &lt;span class="nt"&gt;--dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wpdemo &lt;span class="nt"&gt;--dbuser&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root &lt;span class="nt"&gt;--dbpass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root

Success: Generated &lt;span class="s1"&gt;'wp-config.php'&lt;/span&gt; file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course you should replace the above database information with your own. The next step is to actually run the WordPress installer. That looks a bit like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wp core &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://wpdemo.test"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nt"&gt;--title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"WordPress Demo Site"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nt"&gt;--admin_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pretzelhands"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nt"&gt;--admin_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"awesome-p4ssw0rd"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="nt"&gt;--admin_email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hello@pretzelhands.com"&lt;/span&gt;

Success: WordPress installed successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you're done. You can now visit the URL of your WordPress site and take a look at your wonderful new installation!&lt;/p&gt;

&lt;p&gt;That's a lot of parameters to remember. But never fear, &lt;code&gt;wp&lt;/code&gt; is here! Until you remember all these parameters you can also run &lt;code&gt;wp config create --prompt&lt;/code&gt; and &lt;code&gt;wp core install --prompt&lt;/code&gt;. These commands will interactively ask you for all required parameters. Nifty!&lt;/p&gt;

&lt;p&gt;Now let's talk about some other awesome features you might want to have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing plugins
&lt;/h3&gt;

&lt;p&gt;Check! Grab the slug of a plugin and you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wp plugin &lt;span class="nb"&gt;install &lt;/span&gt;advanced-custom-fields

Installing Advanced Custom Fields &lt;span class="o"&gt;(&lt;/span&gt;x.y.z&lt;span class="o"&gt;)&lt;/span&gt;
Downloading installation package from https://downloads.wordpress.org/plugin/advanced-custom-fields.x.y.z.zip...
Unpacking the package...
Installing the plugin...
Plugin installed successfully.
Success: Installed 1 of 1 plugins.

&lt;span class="nv"&gt;$ &lt;/span&gt;wp plugin activate advanced-custom-fields
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pass in a &lt;code&gt;--activate&lt;/code&gt; flag to activate the plugin immediately. This looks like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wp plugin &lt;span class="nb"&gt;install &lt;/span&gt;advanced-custom-fields &lt;span class="nt"&gt;--activate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't know the slug of a plugin you can find it by going to the &lt;a href="https://wordpress.org/plugins/"&gt;plugin repository&lt;/a&gt;, searching for your plugin and then checking what it says after the last slash. &lt;/p&gt;

&lt;p&gt;For example the URL for the Advanced Custom Fields plugin looks like this: &lt;code&gt;https://wordpress.org/plugins/advanced-custom-fields&lt;/code&gt; If you check everything after the last slash you can see the slug is &lt;code&gt;advanced-custom-fields&lt;/code&gt;. This is what you pass to &lt;code&gt;wp&lt;/code&gt;. It may take some time to remember what slug each plugin has, but you'll remember your favorites soon enough!&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing languages
&lt;/h3&gt;

&lt;p&gt;I hail from a German-speaking country. If I hand over a WordPress site with an English dashboard to a local client, they'll probably look at me weird. Thankfully, installing languages with &lt;code&gt;wp&lt;/code&gt; is a breeze.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wp core language &lt;span class="nb"&gt;install &lt;/span&gt;de_DE &lt;span class="nt"&gt;--activate&lt;/span&gt;

Downloading translation from https://downloads.wordpress.org/translation/core/x.y.z/de_DE.zip...
Unpacking the update...
Installing the latest version...
Translation updated successfully.
Success: Language installed.
Success: Language activated.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you're done! Go grab some tea or coffee and enjoy your success. The &lt;code&gt;--activate&lt;/code&gt; flag here works the same way it does with plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing a theme
&lt;/h3&gt;

&lt;p&gt;By now I'm sure you've figured out the pattern. But here's how you install a theme and activate it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wp theme &lt;span class="nb"&gt;install &lt;/span&gt;storefront &lt;span class="nt"&gt;--activate&lt;/span&gt;

Installing Storefront &lt;span class="o"&gt;(&lt;/span&gt;x.y.z&lt;span class="o"&gt;)&lt;/span&gt;
Downloading installation package from https://downloads.wordpress.org/theme/storefront.x.y.z.zip...
Unpacking the package...
Installing the theme...
Theme installed successfully.
Activating &lt;span class="s1"&gt;'storefront'&lt;/span&gt;...
Success: Switched to &lt;span class="s1"&gt;'Storefront'&lt;/span&gt; theme.
Success: Installed 1 of 1 themes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now you have Storefront for WooCommerce installed and running. Yes, that's really all there is to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing notes and further reading
&lt;/h3&gt;

&lt;p&gt;The three examples given above showed you the most common use cases for &lt;code&gt;wp&lt;/code&gt;, but you can manage pretty much anything with it. Here's a short excerpt of what you can do without ever touching the WordPress dashboard&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manage users&lt;/li&gt;
&lt;li&gt;Create, update, delete your posts&lt;/li&gt;
&lt;li&gt;Create, update, delete and moderate comments&lt;/li&gt;
&lt;li&gt;Do a search-replace in your database&lt;/li&gt;
&lt;li&gt;Export and import WordPress data&lt;/li&gt;
&lt;li&gt;Manage multi-site networks&lt;/li&gt;
&lt;li&gt;Scaffold code for custom post type, taxonomies, child themes, etc&lt;/li&gt;
&lt;li&gt;Run a custom PHP shell for your WordPress install&lt;/li&gt;
&lt;li&gt;Run a local development server for your Wordpress install&lt;/li&gt;
&lt;li&gt;... (The list &lt;strong&gt;does&lt;/strong&gt; go on for a while longer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you install WooCommerce for example you get another set of commands related just to managing your online store.&lt;/p&gt;

&lt;p&gt;So if you're working with WordPress, especially if you're working with more than 2-3 sites I heartily recommend you go out, install &lt;code&gt;wp&lt;/code&gt; and get more free time out of your day.&lt;/p&gt;

&lt;p&gt;If you want to read more, you can &lt;a href="https://make.wordpress.org/cli/handbook/"&gt;check out the documentation&lt;/a&gt;, it is an excellent reference. Otherwise, a lot can be found out by running &lt;code&gt;wp help&lt;/code&gt; and browsing the man page.&lt;/p&gt;

&lt;p&gt;Enjoy!~&lt;/p&gt;

</description>
      <category>php</category>
      <category>productivity</category>
      <category>devops</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>My planned side projects for 2019</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Thu, 24 Jan 2019 20:33:14 +0000</pubDate>
      <link>https://forem.com/pretzelhands/my-planned-side-projects-for-2019-36pb</link>
      <guid>https://forem.com/pretzelhands/my-planned-side-projects-for-2019-36pb</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by NordWood Themes on Unsplash&lt;/small&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://pretzelhands.com/posts/planned-side-projects-in-2019"&gt;my personal blog.&lt;/a&gt; The topic of today's post was &lt;a href="https://twitter.com/_pretzelhands/status/1087818795316641793"&gt;voted on by Twitter.&lt;/a&gt; It's a mix of the top two choices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's 2019. New year, new me, new possibilities! At least that's what many of us want to get out of it. As for me, I have a simple goal. I want 2019 to be more successful than 2018. &lt;a href="https://pretzelhands.com/posts/how-i-got-into-freelancing"&gt;Given how that went&lt;/a&gt;, that is tall enough order on its own.&lt;/p&gt;

&lt;p&gt;For me 2019 should be the year where I focus on establishing a bit more freedom from the normal hustle. At this point in time I work an amount that is probably not healthy in the long term. I sleep a lot less than I should. And that is not something I want to or even can keep up in the long term. 5-6 hours of sleep and coffee a good combination does not make.&lt;/p&gt;

&lt;p&gt;Instead, the focus for 2019 should be on side projects and growing an audience. You've probably been seeing this quite a bit in January already. I started a blog, I've &lt;a href="https://howmuchdoesthismeetingco.st"&gt;rebuilt a previous one-day build of mine&lt;/a&gt;, I've gotten back into the Twitter game and I'm trying to &lt;a href="https://pretzelhands.com/posts/streaming-as-productivity-multiplier"&gt;take up streaming&lt;/a&gt; more regularly again.&lt;/p&gt;

&lt;p&gt;But what are the projects I'm building? Well, ideas may randomly pop into my head at all times, but the ones that follow I'll try my hardest to focus on right now. Enjoy a curated tour of my brain&lt;/p&gt;

&lt;h2&gt;
  
  
  💵 How Much Does This Meeting Cost
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://howmuchdoesthismeetingco.st"&gt;howmuchdoesthismeetingco.st&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one's a biggie. In the middle of 2018 I built a little site called howmuchdoesthismeetingpay.com. It was meant as a half-joke because I'd been sitting in a lot of meetings and wanted to know how much money actually came from that. So I built a little counter on a nice sunny Saturday and put it online. It actually did pretty well on Product Hunt and people were into it!&lt;/p&gt;

&lt;p&gt;But there was one request that came again and again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Richard, I want to use this with my team. Can you add that?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Well, I got side-tracked by a very lucrative contract but yes. Yes I can. So I put a new prototype version of it online and I'm currently hacking away on adding user accounts and billing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The game plan
&lt;/h3&gt;

&lt;p&gt;The idea is to extend this project into more than a simple cost tracker. I want it to be a tool that can be used to track your meetings from start to finish: Meeting notes, action points, participants tracking, statistics. I want meetings to be as transparent as possible.&lt;/p&gt;

&lt;p&gt;Why? Because I think a lot of meetings are just utterly &lt;strong&gt;frustrating and unnecessary&lt;/strong&gt;. If this were more visible, perhaps companies could cut down on wasting precious time and get back to delivering value to their customers.&lt;/p&gt;

&lt;p&gt;Yes, this is supposed to be a paid service.&lt;/p&gt;

&lt;p&gt;I'll also try to livestream most of the development.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ jinx
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://github.com/pretzelhands/jinx/"&gt;GitHub respository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I accidentally a popular repository. For what it's worth: before I started building jinx I was heavily in the camp of &lt;a href="https://caddyserver.com"&gt;Caddy server&lt;/a&gt; converts. But due to some unfortunate circumstances I was forced to move over to nginx in a pinch. And because I got sick and tired of &lt;code&gt;^R&lt;/code&gt;-ing for the same four commands I wrote a wrapper script.&lt;/p&gt;

&lt;p&gt;Then I put it on dev.to and people loved it. Then I marketed it on Twitter and people loved it. And here we are.&lt;/p&gt;

&lt;h3&gt;
  
  
  The game plan
&lt;/h3&gt;

&lt;p&gt;I want to make jinx as useful as it can be for the largest number of people. Some of the more immediate features I want to add are HTTPS support and improved templating for server configurations.&lt;/p&gt;

&lt;p&gt;Ideally this eventually becomes flexible enough to support some sort of plugin functionality so that people can add their own features and ideas! For now, however, everyone is welcome to make a pull request.&lt;/p&gt;

&lt;p&gt;And no, I haven't gone back to hosting my sites on Caddy yet. 🤓&lt;/p&gt;

&lt;h2&gt;
  
  
  📝 Blogging about everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://pretzelhands.com"&gt;pretzelhands.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was inspired by &lt;a href="https://swizec.com"&gt;Swizec Teller&lt;/a&gt; to pick up blogging and try to write 200 words every day for a few weeks. Well, I'm not good at keeping posts short, but I decided that 200 words/ day or 800-1000 words/week doesn't matter all that much.&lt;/p&gt;

&lt;p&gt;What does however matter is keeping up the habit. And I've done pretty nicely on keeping up the habit so far. I've also found that blogging has immensely increased my audience size in a very short period of time.&lt;/p&gt;

&lt;p&gt;Generally I publish posts on my blog first and then crosspost them on dev.to for maximum reach. This has been successful enough that my dev.to audience is actually about to outgrow my Twitter audience. Within less than a month. Woops! 😅&lt;/p&gt;

&lt;h3&gt;
  
  
  The game plan
&lt;/h3&gt;

&lt;p&gt;My blog is both a project I take to to get wild with programming and to grow my online presence. The engine is hand-written from scratch in PHP with a bunch of loose libraries thrown in. I want to extend it bit by bit to turn it into something I love using.&lt;/p&gt;

&lt;p&gt;Ideally, this can also help me supplement my freelance leads in an organic way. Because while the overarching goal is to be less-dependent on a 60hr/week hustle, I will still need to make money to survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤵👰 Getting married
&lt;/h2&gt;

&lt;p&gt;This isn't really a side project, but it does take up a good chunk of my time and it's probably the personally most important thing to me on this list.&lt;/p&gt;

&lt;p&gt;In May I'm getting married to my wonderful fiancée and she will come to live with me. It's been 3.5 years of a long-distance relationship with 10,000km between us at this point and I'm already looking forward to having her around. I've actually taken off all of May in anticipation for it.&lt;/p&gt;

&lt;p&gt;And I'd love to invite all of you, but I can't host some 1,000 people in my backyard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;These are my goals for 2019. I hope to achieve at least some income out of How Much Does This Meeting Cost by the end of 2019. I don't care if it's $5/month or $500/month. But earning real money from a side project is something I've been wanting to do for a long time. So it's about time I just sat down and &lt;strong&gt;shipped something&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to follow along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/_pretzelhands"&gt;Follow me on Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/pretzelhands"&gt;Follow me on dev.to&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pretzelhands.com/#mc-embedded-subscribe-form"&gt;Subscribe to my newsletter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talk to you soon!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>motivation</category>
      <category>personalnews</category>
    </item>
    <item>
      <title>Live streaming as productivity multiplier</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Sun, 20 Jan 2019 21:04:06 +0000</pubDate>
      <link>https://forem.com/pretzelhands/live-streaming-as-productivity-multiplier-13bj</link>
      <guid>https://forem.com/pretzelhands/live-streaming-as-productivity-multiplier-13bj</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by Nikita Kachanovsky on Unsplash&lt;/small&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href="https://pretzelhands.com/posts/streaming-as-productivity-multiplier" rel="noopener noreferrer"&gt;my personal blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's probably a few of you who know some form or another of this meme&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%2Fpretzelhands.com%2Fassets%2Fposts%2Fstreaming-01.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%2Fpretzelhands.com%2Fassets%2Fposts%2Fstreaming-01.png" alt="20 minutes of coding, 5 hours of coffee breaks - Perfectly balanced"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes this is very much me when it comes to side projects. Don't get me wrong, I love building side projects and really want to get as many things out there as possible. But sometimes the rabbit holes of the internet can be crazy deep and what starts out as a quick check in the documentation can end up in hours of reading about some esoteric programming language you'll never make use of.&lt;/p&gt;

&lt;p&gt;I admit that I might have a bit of a procrastination problem. It happens to the best of us.&lt;/p&gt;

&lt;p&gt;But there's a nice way to fight it and share with other people at the same time: Live streaming while you code. This has been a thing for a while now, but especially since &lt;a href="https://shipstreams.com" rel="noopener noreferrer"&gt;shipstreams.com&lt;/a&gt; by fellow Austrian programmer &lt;a href="https://twitter.com/arminulrich" rel="noopener noreferrer"&gt;Armin Ulrich&lt;/a&gt; showed up, there's been a bit of a boom around it. And I joined in back when it first started.&lt;/p&gt;

&lt;p&gt;Turns out streaming is a bit like pair programming on demand but with more of a social factor. Having someone watch you and help you when you're stuck is like a super power. At the same time you get to talk about what you're doing and interact with people who are actually interested in what you're doing feels both incredibly humbling and motivating.&lt;/p&gt;

&lt;p&gt;Generally there's only like 4-5 people at any one time watching my stream, but knowing that they are spending their free time to see me code makes me a lot more focused and prevents me from accidentally wandering off on tangents of non-productive behavior. In the end it's a bit more exhausting than coding by yourself, but it also feels like I get at least twice as much done in the same time.&lt;/p&gt;

&lt;p&gt;Also whenever you find an error you will be doubly invested to fix it and to keep things moving along. Yes, your code might not always be super pretty while you're live, but at least you get stuff done! And sometimes that's the important thing. You can fix code that has been written, but you can't do anything with code that does not exist.&lt;/p&gt;

&lt;p&gt;So if you ever find yourself in a creative rut, give streaming a shot! Set up an account on Twitch and Shipstreams, &lt;a href="https://shipstreams.com/blog/recommended-twitch-streaming-setup-guide" rel="noopener noreferrer"&gt;learn how to set up your PC/laptop&lt;/a&gt; for streaming and off you go! It doesn't have to be perfect, it doesn't have to be ultra professional. People will be happy enough to watch you either way. It's great fun and maybe you can learn a thing or two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And for a shameless plug: You can &lt;a href="http://pretzelhands.live" rel="noopener noreferrer"&gt;follow me on Twitch&lt;/a&gt; to see when I go live. I'd love to chat with all of you!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>motivation</category>
      <category>devtips</category>
    </item>
    <item>
      <title>I am a full-time freelancer aged 21. I work mostly in PHP and React. Last year I made approximately $75,000 in revenue. AMA!</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Wed, 16 Jan 2019 19:55:48 +0000</pubDate>
      <link>https://forem.com/pretzelhands/i-am-a-full-time-freelancer-aged-21-i-work-mostly-in-php-and-react-last-year-i-made-approximately-75000-in-revenue-ama-13gp</link>
      <guid>https://forem.com/pretzelhands/i-am-a-full-time-freelancer-aged-21-i-work-mostly-in-php-and-react-last-year-i-made-approximately-75000-in-revenue-ama-13gp</guid>
      <description>&lt;p&gt;Okay. That title sounded a bit more braggy than I wanted it to. So if you clicked, I hope I at least garnered your interest.&lt;/p&gt;

&lt;p&gt;If you want to read the journey in detail, you can check out &lt;a href="https://dev.to/pretzelhands/how-i-got-into-freelancing-2437"&gt;my post on how I got into freelancing&lt;/a&gt;. To give you a hint: It involved more luck than I had any right to have and I made some very stupid decisions.&lt;/p&gt;

&lt;p&gt;The starting point of my freelance journey was a bit more than two years ago at the tender age of 19. It's been a lot of ups and downs since then, but especially since Q1 2018 I haven't really had any downs.&lt;/p&gt;

&lt;p&gt;I'd be happy to answer any questions you might have regarding freelancing, regarding my chosen tech stack, my previous work and really anything else you might have in mind. As long as I have something to say about it, I'll be glad to tell you.&lt;/p&gt;

&lt;p&gt;You can also find some extra tidbits about me on my profile. :)&lt;/p&gt;

&lt;p&gt;Looking forward to your questions!&lt;/p&gt;

</description>
      <category>ama</category>
      <category>career</category>
    </item>
    <item>
      <title>Breaking the rules to keep programming fun and experimental</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Tue, 15 Jan 2019 19:03:59 +0000</pubDate>
      <link>https://forem.com/pretzelhands/breaking-the-rules-to-keep-programming-fun-and-experimental-1982</link>
      <guid>https://forem.com/pretzelhands/breaking-the-rules-to-keep-programming-fun-and-experimental-1982</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by Ashim D’Silva on Unsplash&lt;/small&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href="https://pretzelhands.com/posts/breaking-the-rules"&gt;my personal blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You probably have processes at work. Style guides for writing your code. Unit tests and integration tests verifying each piece of functionality. Best practices established in the project over a commit history in the thousands. The sacred rules established in your company and by the programming community at large. And following those rules is important for a product used by potentially thousands or millions of users.&lt;/p&gt;

&lt;p&gt;But sometimes, in your personal projects, you should try throwing the rules out of the window. Just chuck them out and play around! One such example is the code &lt;a href="https://pretzelhands.com"&gt;behind my blog&lt;/a&gt;. It's written in PHP without a framework of any kind. It's a loose collection of loose PHP files with a bunch of rewrite rules holding it together. It's basically like a non-polished version of Jekyll. &lt;/p&gt;

&lt;p&gt;Here's what the folder structure looks like using &lt;code&gt;tree&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;blog
├── bootstrap.php
├── composer.json
├── composer.lock
├── lib
│   └── Post.php
├── ps
│   ├── 2019-01-02-coloring-terminal-text.md
│   ├── 2019-01-03-non-obvious-behaviors.md
│   ├── 2019-01-04-command-line-flags.md
│   ├── 2019-01-05-bringing-your-entire-infrastructure-down.md
│   ├── 2019-01-07-jinx.md
│   ├── 2019-01-09-how-i-got-into-freelancing.md
│   ├── 2019-01-12-mentoring-junior-team-members.md
│   ├── 2019-01-14-building-responsive-email-with-mjml.md
│   └── 2019-01-15-breaking-the-rules.md
├── public
│   ├── assets/ (images etc.)
│   ├── blog.php
│   └── index.php
├── vendor/ (dependencies)
└── views
    ├── base.mustache
    ├── blog.mustache
    ├── listing.mustache
    └── partials
        ├── about-me.mustache
        ├── description.mustache
        ├── header.mustache
        ├── newsletter-form.mustache
        └── title.mustache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;public&lt;/code&gt; folder is what gets exposed. The &lt;code&gt;ps&lt;/code&gt; folder contains all of my posts written in Markdown with frontmatter. The &lt;code&gt;lib&lt;/code&gt; folder holds the class responsible for loading posts.&lt;/p&gt;

&lt;p&gt;Internally a URL like &lt;code&gt;https://pretzelhands.com/posts/jinx&lt;/code&gt; gets rewritten to &lt;code&gt;https://pretzelhands.com/blog .php?slug=jinx&lt;/code&gt;. I then search the files to match the correct one based on the slug. I parse the post date from the title. All of that happens in a Post class. Before that the code was directly in blog.php along with some nasty mixed up HTML. &lt;/p&gt;

&lt;p&gt;Here's what that looked like&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YzlHwpjm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pretzelhands.com/assets/posts/rules-01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YzlHwpjm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pretzelhands.com/assets/posts/rules-01.png" alt="The first version of the post-fetching code"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Is it ugly code? Unbelievably so. Was it fun to write? You bet! The entire blogging system was running in an hour or two on a Sunday before going back to work. I didn't want it to be pretty, I just wanted to get it out there. And whenever some code becomes unbearable to look at some selective refactoring happens.&lt;/p&gt;

&lt;p&gt;And I love hacking on this thing. I bolt on some code, upload it to my server and call it a day. There's some globals running wild, there's code duplication, there's all sorts of stuff I would get yelled at for during a code review. But no one will ever see the code for my little blog, so I'm totally okay with doing this. It's my own messy little playground. I keep adding to it and see what sticks.&lt;/p&gt;

&lt;p&gt;So if you're ever working on a small project of your own that isn't intended to be The Next Big Thing™, why don't you just go all out and write code as if you're throwing color at the wall? It's incredibly therapeutic, I tell you!&lt;/p&gt;

&lt;p&gt;Here's an example from a project didn't see the light of day&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;require_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'../../server/bootstrap.php'&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;Respect\Validation\Validator&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;Carbon\Carbon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$database&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getDatabaseConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$emailValidator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Validator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&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="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$emailValidator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'errors'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'Please provide a valid email for logging in.'&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'errorFields'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'email'&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;400&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;



&lt;p&gt;There's no standardization to any of this. The &lt;code&gt;sendResponse&lt;/code&gt; function was a little wrapper that just sent a few headers, serialized the array into JSON and echoed it all. &lt;code&gt;$request&lt;/code&gt; is just the &lt;code&gt;STDIN&lt;/code&gt; PHP provides wrapped in &lt;code&gt;json_decode&lt;/code&gt; It's beautiful in its ugliness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;This does not mean I think you should write production code as if you're some abstract artist and ignore all rules and conventions at work. Those conventions usually have good reasons behind them. But I feel like if you're programming for your own enjoyment it helps to be more creative and experimental. To get rid of rigid structures. Who knows, maybe you find something you really like and can refine and improve.&lt;/p&gt;

&lt;p&gt;So don't go around and tell people that Richard gave you the permission to write spaghetti code. 😁&lt;/p&gt;

&lt;p&gt;Enjoy!~&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>html</category>
      <category>css</category>
      <category>php</category>
    </item>
    <item>
      <title>Building responsive email with MJML</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Mon, 14 Jan 2019 20:23:18 +0000</pubDate>
      <link>https://forem.com/pretzelhands/building-responsive-email-with-mjml-5dhc</link>
      <guid>https://forem.com/pretzelhands/building-responsive-email-with-mjml-5dhc</guid>
      <description>&lt;p&gt;&lt;small&gt;(Photo by Joanna Kosinska on Unsplash)&lt;/small&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href="https://pretzelhands.com/posts/building-responsive-email-with-mjml"&gt;my personal blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I assume that if you're reading this article, you belong to one of three categories of people: The type which has never built HTML email but wants to get into it, the type which has built HTML email before and wants to find a better way to do it and then the type who just reads everything I write for some reason.&lt;/p&gt;

&lt;p&gt;If you count yourself among those that have never built an HTML email before, &lt;a href="https://www.emailonacid.com/blog/article/email-development/email-development-best-practices-2/"&gt;count your blessings&lt;/a&gt;. The amount of work that has to go into these magical creations can be absolutely mind-numbing. HTML email is like &lt;a href="https://www.campaignmonitor.com/dev-resources/guides/coding-html-emails/"&gt;the Wild West of web technologies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I wish I was kidding about this, I really do. Building HTML email was one of my responsibilities at my first job before &lt;a href="https://pretzelhands.com/posts/how-i-got-into-freelancing"&gt;I got into freelancing&lt;/a&gt; and I feel like in those three years I aged by at least a decade. The amount of tiny, obscure things you have to remember is incredible. An affront to everyone even. We're talking inline styles, table layouts, transparency hacks, and so and so forth. And funnily enough Google, the company responsible for one of the most advanced browers, is one of the worst offenders.&lt;/p&gt;

&lt;p&gt;No really, &lt;a href="https://www.campaignmonitor.com/css/"&gt;here's a list of CSS support in email&lt;/a&gt;. Be sure you sit well before reading it. It's a hoot and a half.&lt;/p&gt;

&lt;p&gt;Of course for a while now there's been solutions like Foundation for Emails, emailframe.work and many others. But they're either terrible to set up or still require you to remember a good few pitfalls in HTML email. But a while ago, thanks to &lt;a href="https://twitter.com/pugson"&gt;@pugson&lt;/a&gt; I learned about one framework that stood above all others.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mjml.io"&gt;It's called MJML and it rocks my socks.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It came a bit too late for my mainstay in email marketing, but I still use it for transactional emails. And this is your whirlwind guide to building HTML with it! So let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing and running MJML
&lt;/h2&gt;

&lt;p&gt;Installing MJML is quite simple if you have &lt;code&gt;npm&lt;/code&gt; or &lt;code&gt;yarn&lt;/code&gt; installed. You need only run the following command and you're good to go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; mjml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're not part of the web crowd and don't have MJML or if you just want to try it out and save some time, you can also use &lt;a href="https://mjml.io/try-it-live"&gt;their online playground&lt;/a&gt;. I admit that I mostly use that one, simply because it's super convenient to see your email right next to your code.&lt;/p&gt;

&lt;p&gt;To learn how to use the command-line tool in detail, be sure to check out &lt;a href="https://mjml.io/documentation/#command-line-interface"&gt;the excellent documentation.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But as quick summary here are the commands you will most likely want to use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Compile the MJML file and save it to an HTML file&lt;/span&gt;
mjml email.mjml &lt;span class="nt"&gt;-o&lt;/span&gt; email.html

&lt;span class="c"&gt;# Watch an MJML file for changes and recompile automatically&lt;/span&gt;
mjml &lt;span class="nt"&gt;--watch&lt;/span&gt; email.mjml &lt;span class="nt"&gt;-o&lt;/span&gt; email.html

&lt;span class="c"&gt;# Minify the compiled HTML to save space&lt;/span&gt;
mjml &lt;span class="nt"&gt;--config&lt;/span&gt;.minify email.mjml &lt;span class="nt"&gt;-o&lt;/span&gt; email.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building your first email
&lt;/h2&gt;

&lt;p&gt;As an example email to build I will pick one made by yours truly. It's a magic link email I'm using for &lt;a href="https://howmuchdoesthismeetingco.st?ref=blog"&gt;a meeting tracker app&lt;/a&gt; I'm currently building.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FYIedywx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pretzelhands.com/assets/posts/mjml-01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FYIedywx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pretzelhands.com/assets/posts/mjml-01.png" alt="Magic link email layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's not particularly complex, so I think it's a perfect fit for a first go at MJML. First, let's take a look at the entire template as it stands and then let's take it apart bit by bit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mjml&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-head&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;mj-font&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Montserrat"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css?family=Montserrat:400,700"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;mj-preview&amp;gt;&lt;/span&gt;Click the button inside to log into your account&lt;span class="nt"&gt;&amp;lt;/mj-preview&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;mj-attributes&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;mj-text&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt; &lt;span class="na"&gt;line-height=&lt;/span&gt;&lt;span class="s"&gt;"24px"&lt;/span&gt; &lt;span class="na"&gt;padding=&lt;/span&gt;&lt;span class="s"&gt;"0px 48px 16px"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;mj-button&lt;/span&gt; &lt;span class="na"&gt;font-weight=&lt;/span&gt;&lt;span class="s"&gt;"bold"&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt; &lt;span class="na"&gt;background-color=&lt;/span&gt;&lt;span class="s"&gt;"#8e2de2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;mj-divider&lt;/span&gt; &lt;span class="na"&gt;border-color=&lt;/span&gt;&lt;span class="s"&gt;"#8e2de2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;mj-all&lt;/span&gt; &lt;span class="na"&gt;font-family=&lt;/span&gt;&lt;span class="s"&gt;"Montserrat, Helvetica, Arial, sans-serif"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/mj-attributes&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;mj-style&lt;/span&gt; &lt;span class="na"&gt;inline=&lt;/span&gt;&lt;span class="s"&gt;"inline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          .link {
            color: #8e2de2;
            text-decoration: none;
          }
        &lt;span class="nt"&gt;&amp;lt;/mj-style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/mj-head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-body&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;mj-section&lt;/span&gt; &lt;span class="na"&gt;padding-bottom=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;mj-column&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;mj-image&lt;/span&gt;
                        &lt;span class="na"&gt;padding-top=&lt;/span&gt;&lt;span class="s"&gt;"20px"&lt;/span&gt;
                        &lt;span class="na"&gt;padding-bottom=&lt;/span&gt;&lt;span class="s"&gt;"20px"&lt;/span&gt;
                        &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt;
                        &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://howmuchdoesthismeetingco.st/assets/logo-hmdtmc.png"&lt;/span&gt;
                &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;mj-divider&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;mj-text&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Your magic link&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/mj-text&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;mj-text&amp;gt;&lt;/span&gt;
                    Somebody requested a log in from your account. If that was you click the button
                    below to sign into your account.
                &lt;span class="nt"&gt;&amp;lt;/mj-text&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;mj-button&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://howmuchdoesthismeetingco.st/token/abcdefg123456789"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    Log in now
                &lt;span class="nt"&gt;&amp;lt;/mj-button&amp;gt;&lt;/span&gt;


                &lt;span class="nt"&gt;&amp;lt;mj-text&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;br&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    Have a wonderful day,&lt;span class="nt"&gt;&amp;lt;br&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Richard from HMDTMC&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/mj-text&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;mj-divider&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

                &lt;span class="nt"&gt;&amp;lt;mj-text&lt;/span&gt;
                        &lt;span class="na"&gt;padding=&lt;/span&gt;&lt;span class="s"&gt;"8px 48px"&lt;/span&gt;
                        &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"11px"&lt;/span&gt;
                        &lt;span class="na"&gt;line-height=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt;
                        &lt;span class="na"&gt;color=&lt;/span&gt;&lt;span class="s"&gt;"#999999"&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    If the button up doesn't work, you can use this link:
                    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt;
                            &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://howmuchdoesthismeetingco.st/token/abcdefg123456789"&lt;/span&gt;
                            &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        https://howmuchdoesthismeetingco.st/token/abcdefg123456789
                    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;
                    Your login link expires after 1 hour. You can request another one at any time.
                &lt;span class="nt"&gt;&amp;lt;/mj-text&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/mj-column&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/mj-section&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/mj-body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mjml&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, that was a lot of code and if you're not running yet, I'm proud of you. The most complex part is probably right at the beginning in &lt;code&gt;mj-head&lt;/code&gt;, but let's start from the very beginning: the root element.&lt;/p&gt;

&lt;h3&gt;
  
  
  The document structure
&lt;/h3&gt;

&lt;p&gt;MJML is not unlike HTML or XML. It is after all a markup language that tries to spit out HTML. As such, each MJML document begins with a simple root tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mjml&amp;gt;&amp;lt;/mjml&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside this root element you have the same sections you're used to from a regular HTML document. One &lt;code&gt;head&lt;/code&gt; and one &lt;code&gt;body&lt;/code&gt;. They're just prefixed with &lt;code&gt;mj&lt;/code&gt;. So the standard structure looks like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mjml&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-head&amp;gt;&amp;lt;/mj-head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-body&amp;gt;&amp;lt;/mj-body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mjml&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mj-head&lt;/code&gt; is a pretty exciting place where you define all of your styling and meta data! It's probably the most fun part of writing an MJML-based email.&lt;/p&gt;

&lt;h3&gt;
  
  
  The document head
&lt;/h3&gt;

&lt;p&gt;Now let's look at all the weird stuff in the &lt;code&gt;mj-head&lt;/code&gt; and demystify it a bit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mj-head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-font&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Montserrat"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css?family=Montserrat:400,700"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-preview&amp;gt;&lt;/span&gt;Click the button inside to log into your account&lt;span class="nt"&gt;&amp;lt;/mj-preview&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;mj-attributes&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;mj-text&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt; &lt;span class="na"&gt;line-height=&lt;/span&gt;&lt;span class="s"&gt;"24px"&lt;/span&gt; &lt;span class="na"&gt;padding=&lt;/span&gt;&lt;span class="s"&gt;"0px 48px 16px"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;mj-button&lt;/span&gt; &lt;span class="na"&gt;font-weight=&lt;/span&gt;&lt;span class="s"&gt;"bold"&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt; &lt;span class="na"&gt;background-color=&lt;/span&gt;&lt;span class="s"&gt;"#8e2de2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;mj-divider&lt;/span&gt; &lt;span class="na"&gt;border-color=&lt;/span&gt;&lt;span class="s"&gt;"#8e2de2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;mj-all&lt;/span&gt; &lt;span class="na"&gt;font-family=&lt;/span&gt;&lt;span class="s"&gt;"Montserrat, Helvetica, Arial, sans-serif"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/mj-attributes&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;mj-style&lt;/span&gt; &lt;span class="na"&gt;inline=&lt;/span&gt;&lt;span class="s"&gt;"inline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      .link {
        color: #8e2de2;
        text-decoration: none
      }
    &lt;span class="nt"&gt;&amp;lt;/mj-style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mj-head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the very first child of our document head we have a &lt;code&gt;mj-font&lt;/code&gt; tag. It takes in a &lt;code&gt;name&lt;/code&gt; and an &lt;code&gt;href&lt;/code&gt;. If you look at where the &lt;code&gt;href&lt;/code&gt; links to, you'll quickly figure out that all this does is load in a font hosted on Google Fonts. Of course, given the state of affairs, not all email clients support this. But some do and they get rewarded with prettier typography.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mj-font&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Montserrat"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css?family=Montserrat:400,700"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;After this we find an &lt;code&gt;mj-preview&lt;/code&gt; tag that contains some text. This is an invisible preview text that gets displayed in the inbox of some email clients right below the subject of the email. To give you some context, I'm talking about the light-gray text depicted here (in Apple Mail)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--G_JTCgTF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pretzelhands.com/assets/posts/mjml-02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--G_JTCgTF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pretzelhands.com/assets/posts/mjml-02.png" alt="An example of preview text"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;p&gt;You can use this to tease your subscribers about the content of your newsletter or give a summary of the contents. It's a bit of an art to find text that is just long enough to fill out the entire area, but not too long to be cut off by the email client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mj-preview&amp;gt;&lt;/span&gt;Click the button inside to log into your account&lt;span class="nt"&gt;&amp;lt;/mj-preview&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Next up is a special one. &lt;code&gt;mj-attributes&lt;/code&gt; allows you to set default styling for all MJML tags that you're using. You can of course override these at any time. These just exist so you don't have to repeat yourself over and over again.&lt;/p&gt;

&lt;p&gt;Each child of the &lt;code&gt;mj-attributes&lt;/code&gt; tag is another MJML tag such as &lt;code&gt;mjml-text&lt;/code&gt; or &lt;code&gt;mjml-button&lt;/code&gt;. Each of the CSS properties you want to use is defined as an attribute. This may be unusual at first, but you'll get used to it very quickly. There's also a special &lt;code&gt;mj-all&lt;/code&gt; tag, which can be used to apply a style to all elements. (A bit like the CSS &lt;code&gt;*&lt;/code&gt; selector)&lt;/p&gt;

&lt;p&gt;Also note how I define some fallback fonts for the &lt;code&gt;mj-all&lt;/code&gt; tag. This is so that email clients with bad CSS support still have at least a somewhat bearable font to look at.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mj-attributes&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-all&lt;/span&gt; &lt;span class="na"&gt;font-family=&lt;/span&gt;&lt;span class="s"&gt;"Montserrat, Helvetica, Arial, sans-serif"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-text&lt;/span&gt; 
        &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt;
        &lt;span class="na"&gt;line-height=&lt;/span&gt;&lt;span class="s"&gt;"24px"&lt;/span&gt;
        &lt;span class="na"&gt;padding=&lt;/span&gt;&lt;span class="s"&gt;"0px 48px 16px"&lt;/span&gt; 
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mj-attributes&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;And we wrap up our head component with an &lt;code&gt;mj-style&lt;/code&gt; tag. You can use these to write regular CSS and media queries. One special thing to note here is that I added the &lt;code&gt;inline="inline"&lt;/code&gt; attribute to my style tag. This makes MJML search for all elements with a class of &lt;code&gt;link&lt;/code&gt; and adds a style attribute to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- So this element --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;My awesome link!&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Turns into this --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"color: #8e2de2; text-decoration: none;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;My awesome link!&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason why you would want this is once again rooted in the lack of CSS support in email clients. Many only support only basic selectors like &lt;code&gt;a&lt;/code&gt; or &lt;code&gt;h1&lt;/code&gt;, while Gmail just takes your &lt;code&gt;style&lt;/code&gt; tag and &lt;strong&gt;throws it out entirely.&lt;/strong&gt; Yup. That's actually a real thing that happens. Thanks Google.&lt;/p&gt;

&lt;h3&gt;
  
  
  The document body
&lt;/h3&gt;

&lt;p&gt;This is where the meat of your email lives. MJML provides many custom tags to make building email as painless as can be. You can find a list of all tags available &lt;a href="https://mjml.io/documentation/#standard-body-components"&gt;in the documentation.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will highlight the ones I used below, starting with &lt;code&gt;mj-section&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mj-section&lt;/code&gt; is meant to be used as a row in your layout. Imagine it a bit like Bootstraps or Foundations grid row elements. It offers some vertical spacing by default, but you can of course remove it. If you don't need any special thing like that you can of course also just use a &lt;code&gt;div&lt;/code&gt; or &lt;code&gt;table&lt;/code&gt; (and yes, I'm serious about the &lt;code&gt;table&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mj-column&lt;/code&gt; allows you to define one or more columns in your layout. This way you can easily build a grid layout in your email. To have multiple columns next to each other put them into an &lt;code&gt;mj-section&lt;/code&gt; and you're good to go.&lt;/p&gt;

&lt;p&gt;An example of a 2x2 grid could look something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mj-body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-section&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;mj-column&amp;gt;&lt;/span&gt;Row 1, Column 1&lt;span class="nt"&gt;&amp;lt;/mj-column&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;mj-column&amp;gt;&lt;/span&gt;Row 1, Column 2&lt;span class="nt"&gt;&amp;lt;/mj-column&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/mj-section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;mj-section&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;mj-column&amp;gt;&lt;/span&gt;Row 2, Column 1&lt;span class="nt"&gt;&amp;lt;/mj-column&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;mj-column&amp;gt;&lt;/span&gt;Row 2, Column 2&lt;span class="nt"&gt;&amp;lt;/mj-column&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/mj-section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mj-body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;code&gt;mj-image&lt;/code&gt; is an image. You probably knew that. By default MJML adds some styling to it to make it stand out a bit more, but once again, you can easily change that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mj-text&lt;/code&gt; is a generic text container. You can put any &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;h1&lt;/code&gt; or whatever else in there and make it look pretty. If it's just a random blob of text you can also simply put that text in directly as a child.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mj-button&lt;/code&gt; is essentially a link that's styled like a button. Pass in &lt;code&gt;href&lt;/code&gt; as an attribute and you've got a great call-to-action in less than 30 seconds. If that's not speedy email development I don't know what is.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mj-divider&lt;/code&gt; is a bit like an &lt;code&gt;hr&lt;/code&gt;, except it's a lot easier to style. In my email template I used it to separate header and footer from the main content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Styling tags
&lt;/h3&gt;

&lt;p&gt;For each of the tags I named above, you can easily pass in attributes to give them a one-off style that you didn't add in your &lt;code&gt;mj-attributes&lt;/code&gt;. For example I used this tag to style my footer text&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mj-text&lt;/span&gt;
    &lt;span class="na"&gt;padding=&lt;/span&gt;&lt;span class="s"&gt;"8px 48px"&lt;/span&gt;
    &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"11px"&lt;/span&gt;
    &lt;span class="na"&gt;line-height=&lt;/span&gt;&lt;span class="s"&gt;"16px"&lt;/span&gt;
    &lt;span class="na"&gt;color=&lt;/span&gt;&lt;span class="s"&gt;"#999999"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Footer text
&lt;span class="nt"&gt;&amp;lt;/mj-text&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you end up using a set of styles more than once, but it is still not the default style you want, you can also define an &lt;code&gt;mj-class&lt;/code&gt; that allows you to pass styles around like Oprah passes around cars.&lt;/p&gt;

&lt;p&gt;You need only define the following in your &lt;code&gt;mj-attributes&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mj-attributes&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mj-class&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"padded"&lt;/span&gt; &lt;span class="na"&gt;padding=&lt;/span&gt;&lt;span class="s"&gt;"20px"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mj-attributes&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give you class a name and some CSS attributes you want it to use and off you go. &lt;strong&gt;Note however that this only works for MJML tags&lt;/strong&gt;. For regular HTML tags you should make use of &lt;code&gt;mj-style&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;MJML is a great tool that helps you build responsive HTML email much faster than you would otherwise be able to. The predefined components help you get the most annoying parts out of the way so you can focus on the content and layout, instead of fiddling around with CSS hacks.&lt;/p&gt;

&lt;p&gt;Of course, no tool is perfect and neither is MJML. So in the end, always be sure to test your email templates with a tool such as &lt;a href="https://litmus.com/"&gt;Litmus&lt;/a&gt; or &lt;a href="https://www.emailonacid.com/"&gt;EmailOnAcid&lt;/a&gt;. Just to be sure ;-)&lt;/p&gt;

&lt;p&gt;Enjoy!~&lt;/p&gt;

</description>
      <category>email</category>
      <category>webdev</category>
      <category>html</category>
      <category>css</category>
    </item>
    <item>
      <title>My approach to mentoring junior team members</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Sat, 12 Jan 2019 19:50:43 +0000</pubDate>
      <link>https://forem.com/pretzelhands/my-approach-to-mentoring-junior-team-members-1nd2</link>
      <guid>https://forem.com/pretzelhands/my-approach-to-mentoring-junior-team-members-1nd2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href="https://pretzelhands.com/posts/mentoring-junior-team-members"&gt;my personal blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is probably a bit of a weird one. I generally don't talk about my soft skills as I hold the opinion that I am an awkward nerd who sometimes can't even keep English and German apart in his head and thus makes hilariously bad code switching a reality.&lt;/p&gt;

&lt;p&gt;On the other hand, I've recently found myself doing a lot of mentoring during my hours at &lt;code&gt;$BIGCO&lt;/code&gt; that I mentioned in &lt;a href="https://dev.to/posts/how-i-got-into-freelancing"&gt;my previous post about freelancing&lt;/a&gt; and the people I've been speaking with seemed to be into it.&lt;/p&gt;

&lt;p&gt;Things I write here may be complete bollocks, but hey, read the first paragraph again and you'll understand why. I guess this is partly just for me to think things through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be patient with everyone
&lt;/h2&gt;

&lt;p&gt;I don't know if you've ever noticed this with other members in your team, but many people seem oddly annoyed when approached with questions by someone. It may be because they dislike being interrupted by others during work, it may be because they don't like being bothered with someone else's problems. But it's always a bit sad to see when someone has a question and the person they ask rolls their eyes or sighs as they get up from their table.&lt;/p&gt;

&lt;p&gt;Hence I always, always, &lt;strong&gt;always&lt;/strong&gt; try to be patient. I've had many people come to me for a question that took me all of a minute to show them the answer to. They got unstuck and could happily continue their work and I didn't lose any critical amount of productivity in mine. If a question in your team pops up a lot, write it down on an FAQ page.&lt;/p&gt;

&lt;p&gt;And if you ever find yourself losing your train of thought, &lt;a href="https://swizec.com/blog/write-down-everything"&gt;go and read this post by Swizec.&lt;/a&gt; No one will hold it against you if you take a few seconds to write down stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give them tools to solve the problem
&lt;/h2&gt;

&lt;p&gt;One thing that can be nice to do is to not point somebody directly to a solution unless it's critical to your deadline. But if you have leeway in your project try and show them how they can find a solution themselves. They don't know how to get started with checking where things go wrong in their program? Teach them how to use breakpoints to step through their program! They can't figure out why their &lt;code&gt;flexbox&lt;/code&gt; is behaving weirdly? Show them a site like &lt;a href="https://flexbox.help"&gt;flexbox.help&lt;/a&gt; let them describe their goal to you and let them click through it while explaining it to you.&lt;/p&gt;

&lt;p&gt;If they really ran into something bigger they'll come back and ask for help again, but if they can figure it out themselves that feels like a huge win that they had independently of you. It gives a sense of confidence and independence.&lt;/p&gt;

&lt;p&gt;So show them how to get there, but don't just point at the screen and tell them, they should have, obviously used &lt;code&gt;justify-content: space-between&lt;/code&gt; instead of &lt;code&gt;justify-content: space-around&lt;/code&gt;. Let them have their own wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pair programming can help a ton
&lt;/h2&gt;

&lt;p&gt;This is of course dependent of the type of person you're dealing with, but sometimes  of people feel a lot more confident when pair programming. At times it already helps if you're the duck for their rubber duck debugging. While they speak out loud and explain the code to you, often times things will click in their head.&lt;/p&gt;

&lt;p&gt;So if they're open to it and you have a few minutes, sit with them. Let them explain their code, walk through it with them. Sometimes the thought of someone having your back is all that is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listen to the individual
&lt;/h2&gt;

&lt;p&gt;Of course, all of this varies from person to person. Junior devs that joined your team a week ago will most likely need more hand-holding than one who joined two months ago. Some people don't like somebody looking over their shoulder while they code, and that's fine too. For what it's worth I'll fumble through a &lt;code&gt;docker run&lt;/code&gt; command if you just stand their and watch me.&lt;/p&gt;

&lt;p&gt;Each person is different and each one requires a different approach to mentoring. But these are some things I found useful in helping others learn. Maybe you can find some use for yourself.&lt;/p&gt;

&lt;p&gt;And above all, when a question seems trivial to you but insurmountable to your junior always remember: Once upon a time you were new to programming as well. ;)&lt;/p&gt;

&lt;p&gt;Enjoy!~&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>mentoring</category>
      <category>career</category>
      <category>advice</category>
    </item>
    <item>
      <title>What are some things you wish your mentor did?</title>
      <dc:creator>pretzelhands</dc:creator>
      <pubDate>Fri, 11 Jan 2019 06:20:02 +0000</pubDate>
      <link>https://forem.com/pretzelhands/what-are-some-things-you-wish-your-mentor-did-54a3</link>
      <guid>https://forem.com/pretzelhands/what-are-some-things-you-wish-your-mentor-did-54a3</guid>
      <description>&lt;p&gt;To those of you who once had a mentor, please take a stroll down memory lane.&lt;/p&gt;

&lt;p&gt;Those of you who still have a mentor, please think of your current situation.&lt;/p&gt;

&lt;p&gt;Got it? Okay. Now I have a simple question: What do you wish your mentor did better? Should they have been more proactive in helping you? Should they have given you more independence? Should they have bought you pizza every second Thursday?&lt;/p&gt;

&lt;p&gt;I unexpectedly find myself in a new senior role with some people to guide, and I want to do it as well as I possibly can. So if you have any input, please go ahead and tell me about it.&lt;/p&gt;

&lt;p&gt;Thanks!~&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>beginners</category>
      <category>mentoring</category>
    </item>
  </channel>
</rss>
