Honeybadger Developer BlogUseful articles for web developers in Ruby, Javascript, Elixir, and morehttps://www.honeybadger.io/blog2024-03-19T00:00:00+00:00The Honeybadger.io CrewBuilding command-line applications in Pythonhttps://www.honeybadger.io/blog/building-command-line-applications-in-python-a-comprehensive-guide/2024-03-19T00:00:00+00:002024-03-19T01:26:39+00:00Aditya RajDive into a comprehensive guide on implementing command-line applications in Python. This tutorial uses the `argparse` module to explain how arguments, flags, and options work in a command-line application by building a BMI calculator app and implementing the Linux head command in Python.<p>If you are into programming, you might have used commands like <code>cp</code>, <code>mv</code>, <code>cat</code>, etc, to perform different operations using a text interface like bash or Windows PowerShell. This article discusses implementing command-line applications in Python with functionalities such as keyword arguments, flags, positional arguments, and mode selection. It also discusses how to implement the Linux head command in Python.</p>
<h2 id="what-is-a-command-line-application">What is a command-line application?</h2>
<p>A command-line application is a computer program we use from a text interface like Bash, Windows PowerShell, Zsh, Dash, etc. We can execute a command line application by typing the command name and the required arguments.
For example, we can use the <code>mv</code> command to move a file from one directory to another by passing the source and destination locations as shown below.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/command_example.png" alt="mv command example" /></p>
<p>In the above image, we have used the mv command to move the <code>sample_code.py</code> file from the <code>/HoneyBadger/Codes</code> directory to the <code>/HoneyBadger/ProgramCodes</code> directory.
Here, <code>mv</code> is the command line application name, and the two locations are input arguments to the application.</p>
<h2 id="what-are-the-different-types-of-command-line-arguments">What are the different types of command-line arguments?</h2>
<p>In a command-line application, we can use positional arguments, keyword arguments, options, and flags to decide how the application executes. Let us discuss what they are.</p>
<h3 id="positional-arguments-in-a-command-line-application">Positional arguments in a command-line application</h3>
<p>Positional arguments in a command-line application are mandatory values passed to the application in a specific order.
For example, <code>‘/HoneyBadger/Codes/sample_code.py’</code> and <code>‘/HoneyBadger/ProgramCodes’</code> are positional arguments for the above-mentioned <code>mv</code> command. Here, you observe the following things.</p>
<ul>
<li>The <code>mv</code> command cannot work without these two arguments.</li>
<li>The first argument is the source address of the file.</li>
<li>The second argument is the destination directory to which the file will be moved.</li>
<li>The source address will always be written before the destination address for the command to work correctly.</li>
</ul>
<p>Hence, <strong>positional arguments are mandatory arguments that we pass to a command-line application in a specific order for the application to work correctly</strong>.</p>
<h3 id="keyword-arguments-in-a-command-line-application">Keyword arguments in a command-line application</h3>
<p>The keyword arguments in a command-line application are key-value pairs we pass to the application as input. We specify the keyword argument to the command-line application using single hyphen (-) or double hyphen characters (--) followed by the argument name.</p>
<p>The input value to the argument follows the argument name after a space character or assignment operator (=), as shown in the following command.</p>
<div class="highlight"><pre class="highlight shell"><code>application_name <span class="nt">--argument_1</span> value_1 <span class="nt">--argument_2</span> value_2 <span class="nt">--argument_N</span> value_N positional_argument_1 positional_argument_2 positional_argument_M
</code></pre></div>
<p>You can also write the above command as follows.</p>
<div class="highlight"><pre class="highlight shell"><code>application_name <span class="nt">--argument_1</span><span class="o">=</span>value_1 <span class="nt">--argument_2</span><span class="o">=</span>value_2 <span class="nt">--argument_N</span><span class="o">=</span>value_N positional_argument_1 positional_argument_2 positional_argument_M
</code></pre></div>
<p>In the above statement,</p>
<ul>
<li><code>application_name</code> is the command-line application.</li>
<li><code>argument_1</code>, <code>argument_2</code>, <code>argument_N</code> are keyword arguments with values <code>value_1</code>, <code>value_2</code>, and <code>value_N</code> respectively. You can also use the assignment operator or the space character between the argument and its value to assign values to the keyword arguments.</li>
<li><code>positional_argument_1</code>, <code>positional_argument_2</code>, and <code>positional_argument_M</code> are positional arguments for the application <code>application_name</code>.</li>
</ul>
<p>To understand this, consider the following spark-submit command.</p>
<div class="highlight"><pre class="highlight shell"><code>spark-submit <span class="nt">--master</span> yarn <span class="nt">--deploy-mode</span> cluster <span class="nt">--num-executors</span> 10 <span class="nt">--executor-cores</span> 8 example_code.py
</code></pre></div>
<p>We use the spark-submit command to run applications on a spark cluster. In the above command,</p>
<ul>
<li><code>spark-submit</code> is the application name.</li>
<li><code>master</code> is a keyword argument with the value <code>yarn</code>.</li>
<li><code>deploy-mode</code> is a keyword argument with the value <code>cluster</code>.</li>
<li><code>num-executors</code> is a keyword argument with the value <code>10</code>.</li>
<li><code>executor-cores</code> is a keyword argument with the value <code>8</code>.</li>
<li><code>example_code.py</code> is a mandatory positional argument.</li>
</ul>
<p>The keyword arguments in a command line application often come with a default value. Hence, keyword arguments are optional, and we use them only to change the application's default behavior.</p>
<h3 id="what-are-options-in-a-command-line-application">What are options in a command-line application?</h3>
<p>Options are keyword arguments that have predefined allowed values. To understand this, let us look at the spark-submit command again.</p>
<div class="highlight"><pre class="highlight shell"><code>spark-submit <span class="nt">--master</span> yarn <span class="nt">--deploy-mode</span> cluster <span class="nt">--num-executors</span> 10 <span class="nt">--executor-cores</span> 8 example_code.py
</code></pre></div>
<p>Here, the parameter <code>deploy-mode</code> is an option as it can have only two values, i.e., <code>cluster</code> and <code>client</code>. Similarly, <code>master</code> is also an option as it can have two values, i.e., <code>local</code> and <code>yarn</code>.</p>
<h3 id="what-are-flags-in-a-command-line-application">What are flags in a command-line application?</h3>
<p>Flags work as switches or toggles. We use flags to turn on or off specific features of a command-line application. For instance, we use the cp command to copy files from one location to another using the following syntax.</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">cp </span>source_file_location destination_directory
</code></pre></div>
<p>When we execute the above command, the file kept at <code>source_file_location</code> is copied to <code>destination_directory</code>. If <code>source_file_location</code> is a directory, the <code>cp</code> command won't copy the directory. To change this behavior, we use the <code>-r</code> flag to copy directories using the <code>cp</code> command, as shown below.</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">cp</span> <span class="nt">-r</span> source_file_location destination_directory
</code></pre></div>
<p>In the above command, <code>-r</code> is a flag that enables copying the contents at <code>source_file_location</code> to <code>destination_directory</code> even if <code>source_file_location</code> is a directory. Hence, flags are used in a command line application to turn specific features on or off.</p>
<h2 id="how-to-build-command-line-applications-in-python">How to build command-line applications in Python</h2>
<p>Usually, we execute a Python program from the command-line terminal using the syntax <code>python filename.py</code> or <code>python3 filename.py</code>. For example, suppose that we have the following Python code in a file <code>cmd_example_1.py</code>.</p>
<div class="highlight"><pre class="highlight python"><code><span class="k">print</span><span class="p">(</span><span class="s">"I am a simple python application containing only the print statements."</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"I have been executed through command line."</span><span class="p">)</span>
</code></pre></div>
<p>We can execute the above file from the command line terminal using the following statement.</p>
<div class="highlight"><pre class="highlight shell"><code>python3 cmd_example_1.py
</code></pre></div>
<p>The output looks as follows.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/first_print_example.png" alt="print using Python3 command" /></p>
<p>The above approach to executing the Python application from the command-line interface has some requirements.</p>
<ul>
<li>The user needs to know the filename and exact location of the Python file. If we know the filename and the file's location, we can execute the file by opening the directory where the file is present. Or, we can pass the entire file path of the python file to the python3 command.</li>
<li>If we don’t know the exact location of the Python file, we cannot execute it. If we try to run the file from a different directory, we will get an error saying that the file doesn’t exist, as shown below.</li>
</ul>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/print_error.png" alt="Error in Python3 command" /></p>
<p>In the above image, you can observe that when we move out of the directory where the python file exists, we cannot execute the file unless we pass the full path of the file to the <code>python3</code> command.</p>
<h3 id="what-is-the-solution-to-the-above-problem">What is the solution to the above problem?</h3>
<p>Using the <code>python3</code> command with the filename isn’t very desirable. We don’t need the user to know which Python file is executed for a given command-line application or where the file is stored. Hence, we will hide the entire <code>Python3</code> command using an alias. For this, we will use the <code>alias</code> command in Linux.
The Linux <code>alias</code> command replaces one string from the command-line with another string. To build an alias for our Python application, we will use the <code>alias</code> command to create an alias for the <code>python3</code> command using the following statement.</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">alias </span><span class="nv">print_with_python</span><span class="o">=</span><span class="s1">'python3 /home/aditya1117/HoneyBadger/cmd_example_1.py'</span>
</code></pre></div>
<p>After executing the above statement, whenever we run the command <code>print_with_python</code>, the bash shell will execute the statement <code>"python3 /home/aditya1117/HoneyBadger/cmd_example_1.py"</code> as shown below.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/print_with_python.png" alt="print commmand-line application" /></p>
<p>Hence, we can execute the Python application without knowing the file name. Also, we have used the complete file path of the Python file while creating the alias for the command. Hence, we can execute the application <code>print_with_python</code> in any working directory to run the Python program.
We have successfully created a basic command-line application, <code>print_with_python</code>, that prints two predefined statements when executed.</p>
<h2 id="adding-functionalities-to-the-command-line-application">Adding functionalities to the command-line application</h2>
<p>To add different functionalities to the Python command-line application, we will use the <code>argparse</code> module. You can install this module using PIP by executing the following statement in the bash terminal.</p>
<div class="highlight"><pre class="highlight shell"><code>pip3 <span class="nb">install </span>argparse
</code></pre></div>
<p>After installing, we can use the <code>argparse</code> module in a Python program to create and parse command-line arguments for adding different functionalities.</p>
<h3 id="how-to-create-and-parse-command-line-arguments-in-python">How to create and parse command-line arguments in Python</h3>
<p>We'll create an argument parser to create or parse command-line arguments in a Python application using the <code>ArgumentParser()</code> function defined in the <code>argparse</code> module. The <code>ArgumentParser()</code> function takes a string as input to its <code>description</code> parameter and returns an <code>ArgumentParser</code> object that you can use to define and process arguments.
After creating an <code>ArgumentParser</code> object, we can use the <code>add_argument()</code> and <code>parse_args()</code> methods to create and parse command-line arguments in a Python application.</p>
<h3 id="creating-command-line-arguments-in-a-python-application">Creating command-line arguments in a Python application</h3>
<p>After creating the <code>ArgumentParser</code> object using the <code>ArgumentParser()</code> function, we use the <code>add_argument()</code> method to create arguments for the Python application. It has the following syntax.</p>
<div class="highlight"><pre class="highlight python"><code><span class="n">ArgumentParser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="n">name</span> <span class="ow">or</span> <span class="n">flags</span> <span class="p">[,</span> <span class="n">default</span><span class="p">][,</span> <span class="nb">type</span><span class="p">][,</span> <span class="n">choices</span><span class="p">][,</span> <span class="n">required</span><span class="p">][,</span> <span class="n">help</span><span class="p">][,</span> <span class="n">dest</span><span class="p">]</span> <span class="p">[,</span><span class="n">action</span><span class="p">][,</span><span class="n">const</span><span class="p">]</span> <span class="p">)</span>
</code></pre></div>
<p>Here,</p>
<ul>
<li>The <code>name</code> or <code>flags</code> parameter takes as its input the name of the command-line argument that we want to create. If we're going to create positional arguments, we pass a single string to the <code>name</code> parameter. You can pass multiple strings to the <code>name</code> or <code>flags</code> parameter to create keyword arguments, optional parameters, or flags.</li>
<li>The <code>default</code> parameter defines the default value for the input argument if the argument is absent from the command line.</li>
<li>The <code>type</code> parameter defines the data type for the values passed to the argument.</li>
<li>We use the <code>choices</code> parameter to define all the allowed values for an input argument.</li>
<li>The <code>required</code> parameter in the <code>add_argument()</code> method is used to specify if a keyword argument can be omitted while executing the application.</li>
<li>We use the <code>help</code> parameter to specify a brief description of what the argument does.</li>
<li>Instead of creating variables with the same name as the input arguments, we can map the command-line argument to different variable names. The <code>dest</code> parameter takes a command-line argument's destination variable name as its input. While parsing the command-line arguments, the values for the given command-line arguments are stored in the variable name passed to the <code>dest</code> parameter in the <code>add_argument()</code> method. After this, we can access the values using the variable name in the <code>dest</code> parameter.</li>
<li>The <code>action</code> parameter defines the action to be taken when the argument is encountered at the command line. We can use this parameter to define flags.</li>
<li>We use the <code>const</code> parameter to define the constant values the <code>action</code> parameter requires.</li>
</ul>
<p>The <code>add_argument()</code> method, when invoked on an <code>ArgumentParser</code> object, creates the argument as defined in the input parameters.</p>
<h3 id="parse-command-line-arguments-in-a-python-application">Parse command-line arguments in a Python Application</h3>
<p>To parse the command-line arguments created by the <code>add_argument()</code> method, we use the <code>parse_args()</code> method. The <code>parse_args()</code> method, when invoked on an <code>ArgumentParser</code> object, returns a <code>Namespace</code> object containing the command-line arguments as its variables.
We can retrieve values in the arguments using the <code>Namespace</code> object and the argument names. For this, we use the syntax <code>namespace.argument_name</code>, as discussed in the following sections.</p>
<h2 id="handling-arguments-in-a-command-line-application-in-python">Handling arguments in a command-line application in Python</h2>
<p>We can add positional arguments, keyword arguments, options, and flags to a command-line application to make it more interactive. Let us discuss how to do this one by one.</p>
<h3 id="add-positional-arguments-to-a-command-line-application">Add Positional Arguments to a command-line application</h3>
<p>To add a positional argument in a command-line application, we first create an <code>ArgumentParser</code> object using the <code>ArgumentParser()</code> function. Then, we invoke the <code>add_argument()</code> method on the <code>ArgumentParser</code> object and pass the argument name as input. After this, the argument is added to the command-line application.
We use the <code>parse_args()</code> method to read the value passed to the argument. Here, we will use the namespace returned by the <code>parse_args()</code> method to get the value passed to the command-line argument.</p>
<p>To understand this, consider the following code.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'first_value'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">first_value</span>
<span class="k">print</span><span class="p">(</span><span class="s">"The input argument is:"</span><span class="p">,</span> <span class="n">value1</span><span class="p">)</span>
</code></pre></div>
<p>In the above code, we have defined the positional argument <code>first_value</code> using the add_argument() method. When the code executes, the <code>parse_args()</code> method returns a namespace in which <code>first_value</code> is a variable name.</p>
<p>We will save the above code in the file <code>cmd_example_2.py</code>. Then, we will create an alias for running the <code>Python3</code> command using the following statement.</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">alias </span><span class="nv">argument_example</span><span class="o">=</span><span class="s1">'python3 /home/aditya1117/HoneyBadger/cmd_example_2.py'</span>
</code></pre></div>
<p>Next, we will run the program <code>argument_example</code> to get the output as shown below.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/argument_example_1.png" alt="command line argument with positional argument" /></p>
<p>Positional arguments are mandatory. Hence, if you don’t pass the argument with the command-line application, the program will run into an error saying that the arguments are required.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/argument_example_error.png" alt="Error while using positional arguments" /></p>
<p>To explain the purpose of the arguments, you can use the <code>help</code> parameter in the <code>add_argument()</code> method. The help messages are printed when the user passes the <code>--help</code> flag to the command-line application, as shown below.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'first_value'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'First input value.'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'second_value'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Second input value.'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">first_value</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">second_value</span>
<span class="k">print</span><span class="p">(</span><span class="s">"The first input argument is:"</span><span class="p">,</span> <span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"The second input argument is:"</span><span class="p">,</span> <span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>When we pass the <code>--help</code> flag to the above program, the output looks as follows.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/argument_help.png" alt="help flag example" /></p>
<p>You can observe that the strings passed to the <code>help</code> parameter in the <code>add_argument()</code> method are present as details for the arguments.</p>
<p>While executing the above command-line application, you must pass all the positional arguments if it doesn't have a default value. Otherwise, the program will run into an error, as shown below.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/argument_example_error_2.png" alt="Error in command-line application" /></p>
<h3 id="add-keyword-arguments-to-the-command-line-application">Add keyword arguments to the command-line application</h3>
<p>We can define keyword arguments in a command-line application using the hyphen character with the keyword names. For example, we can add the keyword arguments <code>keyword_arg1</code> and <code>keyword_arg2</code> to a Python command-line application, as shown below.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'-keyword_arg1'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'First keyword argument.'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'-keyword_arg2'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Second keyword argument.'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">keyword_arg1</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">keyword_arg2</span>
<span class="k">print</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>If no input is passed to the keyword arguments, they are set to None. You can observe this in the following output.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/keyword_argument_1.png" alt="Application with no keyword argument inputs" /></p>
<p>We haven’t passed any arguments to the command-line application in the above example. Hence, both the input arguments are None. When we pass a value to the arguments, it will be reflected in the output.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/keyword_argument_2.png" alt="Application with keyword argument inputs" /></p>
<p>In the above image, you can observe that I used the syntax using the space character and the assignment operator to assign values to the keyword argument.
Single hyphen characters are often used for flags. Hence, we will use the double hyphen (--) to define keyword arguments in our application. However, the working of the application doesn't change. You can observe this in the following example.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--keyword_arg1'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'First keyword argument.'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--keyword_arg2'</span><span class="p">,</span><span class="n">help</span><span class="o">=</span><span class="s">'Second keyword argument.'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">keyword_arg1</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">keyword_arg2</span>
<span class="k">print</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>Output:</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/keyword_argument_alt.png" alt="Alternative way to use keyword arguments" /></p>
<p>In the above example, we have used a double hyphen character to define the keyword arguments instead of a single hyphen character. Still, the functioning of the program remains unchanged.</p>
<h3 id="add-options-to-a-command-line-application">Add options to a command-line application</h3>
<p>We will use the <code>choices</code> parameter in the <code>add_argument()</code> method to define options in a command-line application. The <code>choices</code> parameter takes a list of allowed values for a given keyword argument as its input. After this, the keyword argument behaves as an option. To understand this, consider the following code.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--option1'</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="p">[</span><span class="s">"A"</span><span class="p">,</span> <span class="s">"B"</span><span class="p">,</span><span class="s">"C"</span><span class="p">,</span> <span class="s">"D"</span><span class="p">],</span> <span class="n">help</span><span class="o">=</span><span class="s">'Allowed values: "A", "B","C", "D"'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--option2'</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="p">[</span><span class="s">"X"</span><span class="p">,</span> <span class="s">"Y"</span><span class="p">,</span> <span class="s">"Z"</span><span class="p">],</span> <span class="n">help</span><span class="o">=</span><span class="s">'Allowed values: "X", "Y", "Z"'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">option1</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">option2</span>
<span class="k">print</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>In the above code, the argument <code>option1</code> only takes values <code>"A"</code>, <code>"B"</code>, <code>"C"</code>, and "<code>D"</code> as its input. Similarly, the argument <code>option2</code> only takes the values <code>"X"</code>, <code>"Y"</code>, and <code>"Z"</code> as its input. You can observe this in the following example.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/options_example.png" alt="Command-line application with options" /></p>
<p>In the above output, you can observe that the options contain the value None if we don’t pass any input arguments. The application executes normally if we pass the allowed values to each option. But, when we pass unspecified values as input to the options, the program runs into an error saying that you should choose input from specified values.</p>
<h3 id="add-flags-to-a-command-line-application-in-python">Add flags to a command-line application in Python</h3>
<p>We use flags in a command-line application to turn features on or off. Hence, flags mostly contain True or False values. We will use the <code>action</code> parameter in the <code>add_argument()</code> method to create flags. The <code>action</code> parameter takes predefined values given in the <code>argparse</code> module. For example, it takes the strings <code>"store_true"</code> and <code>"store_false"</code> to store True and False values, respectively, in a flag if the flag is present in a command-line application.</p>
<p>Here,</p>
<ul>
<li>Suppose we set the <code>action</code> parameter to <code>"store_true"</code> for a flag. Then, its default value is set to <code>False</code>. It is set to True only when the flag is present in the command-line arguments.</li>
<li>If we set the <code>action</code> parameter to <code>"store_false"</code> for a flag, its default value is set to <code>True</code>. It is set to <code>False</code> only when the flag is present in the command-line arguments.</li>
</ul>
<p>To understand this, consider the following code.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'-flag1'</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s">"store_true"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Flag that becomes True if present in command line'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'-flag2'</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s">"store_false"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Flag that becomes False if present in command line'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">flag1</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">flag2</span>
<span class="k">print</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>When we run the above application with and without flags, the output looks as follows.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/flag_example.png" alt="Command-line application with flags" /></p>
<p>The above image shows that <code>flag1</code> is False and <code>flag2</code> is True when we don't pass the flags in the command-line argument. The values are reversed when we use the flags in the command line.</p>
<h2 id="adding-more-features-to-the-command-line-application">Adding more features to the command-line application</h2>
<p>Apart from the basic inputs, we can perform different operations such as setting default values for the arguments, defining data types, assigning variable names, and making keyword arguments and options <code>required</code> in a command-line application in Python. Let us discuss each operation one by one.</p>
<h3 id="set-default-values-for-keyword-arguments">Set default values for keyword arguments</h3>
<p>We'll use the <code>default</code> parameter in the <code>add_argument()</code> method to set default values for the keyword arguments in a command-line application in Python. If a value isn’t passed to an argument, the input passed to the <code>default</code> parameter is assigned to the argument. To understand this, consider the following example.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--keyword_arg1'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s">"Aditya"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'First keyword argument.'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--keyword_arg2'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span> <span class="s">"Raj"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Second keyword argument.'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">keyword_arg1</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">keyword_arg2</span>
<span class="k">print</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>In the above code, we have assigned the default values <code>"Aditya"</code> and <code>"Raj"</code> to the arguments <code>keyword_arg1</code> and <code>keyword_arg2</code>. Hence, it will contain the default values if we don't use these arguments while executing the command-line application. When we pass any value to these arguments, it will be processed accordingly. You can observe this in the following outputs.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/default_example.png" alt="Command line application with default values" /></p>
<h3 id="set-data-types-for-input-arguments">Set data types for input arguments</h3>
<p>The command-line arguments are read as strings in a Python application by default. However, we can specify data types for the input arguments. For this, we use the type parameter in the add_argument() method, as shown below.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--name'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Name of the user'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--age'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span> <span class="nb">int</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Age of the user'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">name</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">age</span>
<span class="k">print</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>In the above code, we have specified the data type <code>str</code> for the <code>name</code> argument and <code>int</code> for the <code>age</code> argument. After this, the program checks for the data type of the input argument. If the input value isn’t compatible with the defined data type, the program runs into an error. You can observe this in the following output.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/argument_with_type.png" alt="Command-line application with argument data types" /></p>
<h3 id="make-keyword-arguments-required">Make keyword arguments required</h3>
<p>Some input arguments can never be optional. For example, you cannot leave the weight and height of a user empty in an application calculating BMI.</p>
<p>To make the keyword arguments required in a command-line application in Python, we set the <code>required</code> parameter to True in the <code>add_argument()</code> method while defining the arguments. After this, the program will work correctly only when the user passes the required arguments to the command-line application.</p>
<p>To understand this, let us implement a BMI calculator command-line application.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--name'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Name of the user'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--age'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span> <span class="nb">int</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Age of the user'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--height'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Height of the user in cm'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--weight'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span> <span class="nb">int</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Weight of the user'</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">name</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">age</span>
<span class="n">user_weight</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">weight</span>
<span class="n">user_height</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">height</span>
<span class="n">BMI</span><span class="o">=</span> <span class="n">user_weight</span><span class="o">/</span><span class="p">(</span><span class="n">user_height</span><span class="o">/</span><span class="mi">100</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span>
<span class="k">print</span><span class="p">(</span><span class="s">"The BMI for {} is {}."</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">value1</span><span class="p">,</span><span class="n">BMI</span><span class="p">))</span>
</code></pre></div>
<p>In the above code, the <code>required</code> parameter is set to True while defining the <code>name</code>, <code>height</code>, and <code>weight</code> arguments. Hence, the program will only work when the required arguments are passed to the command-line application during execution.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/required_argument.png" alt="Command-line application with required arguments" /></p>
<h3 id="separate-variables-from-arguments-in-the-application">Separate variables from arguments in the application</h3>
<p>By default, the arguments in the command-line application are passed as variables in the namespace returned by the <code>parse_args()</code> method. However, we can change this behavior and define a different variable name for each command-line argument. We use the <code>dest</code> parameter in the <code>add_argument()</code> method. The <code>dest</code> parameter takes the name of the destination for storing the value of a given command line argument. After this, the value for the given command-line argument is stored in the variable with the name passed to the <code>dest</code> parameter in the <code>add_argument()</code> method. You can observe this in the following example.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'Parser for command line application'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--optional_value1'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'First optional value.'</span><span class="p">,</span> <span class="n">dest</span><span class="o">=</span><span class="s">"first_value"</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--optional_value2'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Second optional value.'</span><span class="p">,</span> <span class="n">dest</span><span class="o">=</span><span class="s">"second_value"</span><span class="p">)</span>
<span class="n">arguments</span><span class="o">=</span><span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">value1</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">first_value</span>
<span class="n">value2</span><span class="o">=</span><span class="n">arguments</span><span class="p">.</span><span class="n">second_value</span>
<span class="k">print</span><span class="p">(</span><span class="n">value1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">value2</span><span class="p">)</span>
</code></pre></div>
<p>In the above code, the behavior of the program doesn’t change. It will print the value passed to the input arguments <code>optional_value1</code> and <code>optional_value2</code>. However, the values are stored in the program in the variables <code>first_value</code> and <code>second_value</code> instead of <code>optional_value1</code> and <code>optional_value2</code>. The output remains the same, as shown below.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/argument_dest.png" alt="Command-line application with argument destination" /></p>
<h2 id="implementing-actual-command-line-applications-in-python">Implementing actual command-line applications in Python</h2>
<p>By now, we have covered almost all the tools we need to create working command-line applications in Python. Next, we will discuss implementing the Linux <code>head</code> command using the argparse module in Python.</p>
<h3 id="implementing-the-linux-head-command-in-python">Implementing the Linux head command in Python</h3>
<p>The Linux <code>head</code> command is used to display the contents of a file. By default, it shows the first ten lines of any given file. However, we can set the argument n to any number to display that many lines from the file. If the value passed to the argument n is greater than the total number of lines in the file, the program prints the total number of lines. You can observe this in the following example.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/head_example.png" alt="Linux head command example" /></p>
<p>In the above example, we have used the following text file generated using content from ChatGPT.</p>
<p><a href="honebadger_details.txt">honebadger_details.txt</a></p>
<p>In the output, you can observe that the <code>head</code> command displays ten text file lines. When we set n to 5, it shows only five lines from the file. Finally, if we give a non-existent filename to the <code>head</code> command, it says there is no such file or directory.</p>
<p>Let us implement this behavior in Python.</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">import</span> <span class="nn">argparse</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s">'The Linux head command in Python'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'path'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'Address of the file to read.'</span><span class="p">)</span>
<span class="n">parser</span><span class="p">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">'--n'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'How many lines of the file to read'</span><span class="p">,</span> <span class="n">dest</span><span class="o">=</span><span class="s">"num_lines"</span><span class="p">)</span>
<span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span>
<span class="n">filepath</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">path</span>
<span class="n">num_lines</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">num_lines</span>
<span class="k">if</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">filepath</span><span class="p">):</span>
<span class="nb">file</span><span class="o">=</span><span class="nb">open</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span><span class="s">"r"</span><span class="p">)</span>
<span class="n">content</span><span class="o">=</span><span class="nb">file</span><span class="p">.</span><span class="n">readlines</span><span class="p">()</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_lines</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="n">content</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>
<span class="k">except</span> <span class="nb">IndexError</span><span class="p">:</span>
<span class="k">pass</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"The file doesn't exist."</span><span class="p">)</span>
</code></pre></div>
<p>In the above code, we first define the command-line arguments to read the filename and the number of lines. Then we used the <code>isfile()</code> method in the <code>os</code> module to check whether the file with the input filename exists. If the file exists, we read the n lines of the file. Otherwise, we inform the user that the file doesn't exist.</p>
<p>Let us create a command-line application <code>head_command</code> using the Linux <code>alias</code> command and the above Python code. When we execute the <code>head_command</code> application, the output looks as follows.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/head_command_1.png" alt="head command using python example" /></p>
<p>By default, the <code>head_command</code> application reads ten lines from the text file as the default value for the argument <code>n</code> is set to 10. If we set the argument <code>n</code> to a specific number, it will read only that many lines. If the file passed to the <code>head_command</code> application doesn't exist, the application will say so. You can observe this in the following example.</p>
<p><img src="/images/blog/posts/building-command-line-applications-in-python-a-comprehensive-guide/head_command_2.png" alt="head command using python example 2" /></p>
<p>Thus, we have successfully imitated the Linux <code>head</code> command using the <code>argparse</code> module and the <code>os</code> module in Python.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this article, we discussed different elements of a command-line application. Then, we discussed how to implement all the elements of a command-line application, like positional arguments, keyword arguments, flags, options, etc. We also discussed implementing a BMI calculator command-line application and the Linux <code>head</code> command in Python.</p>
<p>I hope you enjoyed reading this tutorial. To brush up your skills, use Python to implement more commands like <code>cp</code>, <code>mv</code>, <code>cat</code>, etc. This will help you understand how to use all the elements of a command-line application.</p>
<p>Stay tuned for more informative articles. Happy Learning!</p>
Let's build a Hanami apphttps://www.honeybadger.io/blog/build-hanami-app/2024-03-12T00:00:00+00:002024-03-19T01:26:39+00:00Aestimo KirinaHanami is a fresh take on building full-stack Ruby apps. With its focus on abstractions, minimal defaults, and speed, it could be the framework to take on Rails' dominance of the Ruby landscape. In this tutorial, we build a simple app using Hanami and, along the way, learn the framework and how it works.<p><a href="https://hanamirb.org/">Hanami</a> is a full-stack Ruby web framework. Unlike Rails, which has many default assumptions about how an app should be built, Hanami promises developer freedom by not imposing too many such defaults.</p>
<p>The framework is also blazingly fast due to its low memory footprint and focus on minimalism. Combine that with a focus on strict abstractions, and you get a fully-featured Ruby framework that could rival Rails for building some applications, such as APIs and micro-services.</p>
<p>In this tutorial, we'll learn about the framework's structure and features as we go through the steps of building a simple blog application.</p>
<p>Let's get started.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>In order to follow along with this tutorial, ensure you have the following ready to go:</p>
<ul>
<li>Ruby 3.0+ installed in your development environment.</li>
<li>A local installation of Postgresql</li>
</ul>
<p>That's it.</p>
<h2 id="installing-hanami-and-generating-a-new-application">Installing Hanami and generating a new application</h2>
<p>Hanami can be installed by running the command:</p>
<div class="highlight"><pre class="highlight shell"><code>gem <span class="nb">install </span>hanami
</code></pre></div>
<p>Next, generate a new Hanami app with the command below:</p>
<div class="highlight"><pre class="highlight shell"><code>hanami new hanami_blog_app
</code></pre></div>
<p>This should give you an app directory structure like the one shown below:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">.</span>
├── app
│ ├── action.rb
│ └── actions
├── config
│ ├── app.rb
│ ├── puma.rb
│ ├── routes.rb
│ └── settings.rb
├── config.ru
├── Gemfile
├── Gemfile.lock
├── Guardfile
├── lib
│ ├── hanami_blog_app
│ │ └── types.rb
│ └── tasks
├── Rakefile
├── README.md
└── spec
├── requests
│ └── root_spec.rb
├── spec_helper.rb
└── support
├── requests.rb
└── rspec.rb
10 directories, 16 files
</code></pre></div>
<p>As you can see, Hanami's directory structure is very similar to what Rails gives you, but there are notable differences:</p>
<ul>
<li><strong>app</strong> - Most of your application's code will go here.</li>
<li><strong>lib</strong> - Here's where you'll put any code to support your app's main codebase.</li>
<li><strong>config</strong> - This is configuration files will go e.g. Puma configurations, routes and other app settings.</li>
</ul>
<p>To get a complete picture of the Hanami directory structure, see the project's <a href="https://guides.hanamirb.org/v2.0/introduction/getting-started/">documentation</a>; for now, we'll leave it at that.</p>
<p>Before proceeding, it's important to note that Hanami version 2.0 (current version as of this writing) does not yet have the Hanami view integrated, so in order to generate views later in the tutorial, you'll need to add the Hanami view gem in your app's Gemfile:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># Gemfile</span>
<span class="o">...</span>
<span class="n">gem</span> <span class="s1">'hanami-view'</span><span class="p">,</span> <span class="ss">github: </span><span class="s1">'hanami/view'</span><span class="p">,</span> <span class="ss">branch: </span><span class="s1">'main'</span>
</code></pre></div>
<p>Then run <code>bundle install</code>. Now, we're ready to outline the app we'll be working on next.</p>
<h2 id="the-app-well-be-building">The app we'll be building</h2>
<p>In this tutorial, we'll build a simple blog app that will showcase some of the internals of Hanami and give you a good base from which you can build more robust apps that leverage the framework's strengths.</p>
<p><em>You can find the source code for the completed app <a href="https://github.com/iamaestimo/hanami_2_blog_app">here</a>.</em></p>
<h2 id="adding-persistence-to-a-hanami-app">Adding persistence to a Hanami app</h2>
<p>Since a natively integrated persistence library is planned for the upcoming Hanami 2.1, for now, we'll be using the awesome <a href="https://rom-rb.org/">Ruby Object Mapper</a> (ROM) library to build the persistence layer in the simple blog app.</p>
<h3 id="install-the-rom-library">Install the ROM library</h3>
<p>Open your app's <code>Gemfile</code> and add the gems listed below, then run <code>bundle install</code>:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># Gemfile</span>
<span class="n">gem</span> <span class="s2">"rom"</span><span class="p">,</span> <span class="s2">"~> 5.3"</span>
<span class="n">gem</span> <span class="s2">"rom-sql"</span><span class="p">,</span> <span class="s2">"~> 3.6"</span>
<span class="n">gem</span> <span class="s2">"pg"</span>
</code></pre></div>
<h3 id="create-the-app-database">Create the app database</h3>
<p>Next, create a Postgresql database via the terminal in your development machine and give it an appropriate name (I called mine "hanami_blog_app"). If you don't know how to do this, just follow the Postgresql guides <a href="https://www.postgresql.org/docs/current">here</a>.</p>
<p><em>Note: It's possible to use the Hanami command <code>createdb <db_name></code> from within your project root, but depending on how you've configured your Postgresql installation, this might work or not.</em></p>
<h3 id="add-a-persistence-provider">Add a persistence provider</h3>
<p>The next step is to add a <em>persistence</em> provider. In Hanami, providers are a way of registering app components that exist outside the app's automatic component registration lifecycle. You can read more about them <a href="https://guides.hanamirb.org/v2.1/app/providers/">here</a>.</p>
<p>Create a new file <code>config/providers/persistence.rb</code> and edit it as shown below:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/providers/persistence.rb</span>
<span class="no">Hanami</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">register_provider</span> <span class="ss">:persistence</span><span class="p">,</span> <span class="ss">namespace: </span><span class="kp">true</span> <span class="k">do</span>
<span class="n">prepare</span> <span class="k">do</span>
<span class="nb">require</span> <span class="s2">"rom"</span>
<span class="n">config</span> <span class="o">=</span> <span class="no">ROM</span><span class="o">::</span><span class="no">Configuration</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:sql</span><span class="p">,</span> <span class="n">target</span><span class="p">[</span><span class="s2">"settings"</span><span class="p">].</span><span class="nf">database_url</span><span class="p">)</span>
<span class="n">register</span> <span class="s2">"config"</span><span class="p">,</span> <span class="n">config</span>
<span class="n">register</span> <span class="s2">"db"</span><span class="p">,</span> <span class="n">config</span><span class="p">.</span><span class="nf">gateways</span><span class="p">[</span><span class="ss">:default</span><span class="p">].</span><span class="nf">connection</span>
<span class="k">end</span>
<span class="n">start</span> <span class="k">do</span>
<span class="n">config</span> <span class="o">=</span> <span class="n">target</span><span class="p">[</span><span class="s2">"persistence.config"</span><span class="p">]</span>
<span class="n">config</span><span class="p">.</span><span class="nf">auto_registration</span><span class="p">(</span>
<span class="n">target</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"lib/hanami_blog_app/persistence"</span><span class="p">),</span>
<span class="ss">namespace: </span><span class="s2">"HanamiBlogApp::Persistence"</span>
<span class="p">)</span>
<span class="n">register</span> <span class="s2">"rom"</span><span class="p">,</span> <span class="no">ROM</span><span class="p">.</span><span class="nf">container</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Providers go through a lifecycle with the steps of <em>prepare</em>, <em>start</em> and <em>stop</em>. In the persistence example above, a prepare step takes care of getting a database connection (which we'll define next), and a start step defines what happens after a connection is established.</p>
<h3 id="adding-the-database-settings">Adding the database settings</h3>
<p>The app's <code>config/settings.rb</code> file is where you define your own app-specific settings and parameters using constructors within the <code>Settings</code> class.</p>
<p>The important thing to note here is that these settings are very different from the <a href="https://guides.hanamirb.org/v2.1/app/app-config/">App config</a> which are configurations that affect the Hanami framework within the app.</p>
<p>To define the app's database connection string, go ahead and modify the settings file like so:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/settings.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">class</span> <span class="nc">Settings</span> <span class="o"><</span> <span class="no">Hanami</span><span class="o">::</span><span class="no">Settings</span>
<span class="n">setting</span> <span class="ss">:database_url</span><span class="p">,</span> <span class="ss">constructor: </span><span class="no">Types</span><span class="o">::</span><span class="no">String</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>And since Hanami uses the dotenv gem to read environment variables in development, you can go ahead and now define the database URL in an <code>.env</code> file in your app's root like so:</p>
<div class="highlight"><pre class="highlight plaintext"><code># .env
DATABASE_URL=postgres://<user>:<password>@localhost:5432/<database_name>
</code></pre></div>
<p>You can confirm that the database connection setting is working by opening a Hanami console session with <code>bundle exec hanami console</code>, and running <code>app["settings"].database_url</code>, which should return the database connection URL you've just defined in the <code>.env</code> file.</p>
<h3 id="creating-and-running-the-first-migration">Creating and running the first migration</h3>
<p>As mentioned before, once ROM is integrated into the upcoming Hanami, migration commands will be included as well. In the meantime, let's create one manually using some rake tasks.</p>
<p>Open up the Rakefile and edit as shown below:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># Rakefile</span>
<span class="o">...</span>
<span class="nb">require</span> <span class="s2">"rom/sql/rake_task"</span>
<span class="n">task</span> <span class="ss">:environment</span> <span class="k">do</span>
<span class="nb">require_relative</span> <span class="s2">"config/app"</span>
<span class="nb">require</span> <span class="s2">"hanami/prepare"</span>
<span class="k">end</span>
<span class="n">namespace</span> <span class="ss">:db</span> <span class="k">do</span>
<span class="n">task</span> <span class="ss">setup: :environment</span> <span class="k">do</span>
<span class="no">Hanami</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">prepare</span><span class="p">(</span><span class="ss">:persistence</span><span class="p">)</span>
<span class="no">ROM</span><span class="o">::</span><span class="no">SQL</span><span class="o">::</span><span class="no">RakeSupport</span><span class="p">.</span><span class="nf">env</span> <span class="o">=</span> <span class="no">Hanami</span><span class="p">.</span><span class="nf">app</span><span class="p">[</span><span class="s2">"persistence.config"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Then, generate the first migration with the following command:</p>
<div class="highlight"><pre class="highlight shell"><code>bundle <span class="nb">exec </span>rake db:create_migration[create_posts]
</code></pre></div>
<p>In case you have a <code>zsh</code> shell, the above command will not work since you'll need to escape the square brackets:</p>
<div class="highlight"><pre class="highlight shell"><code>bundle <span class="nb">exec </span>rake db:create_migration<span class="se">\[</span>create_posts<span class="se">\]</span>
</code></pre></div>
<p>Running that should create a new <code>db/migrate</code> folder with a new migration file in it:</p>
<div class="highlight"><pre class="highlight shell"><code>db
└── migrate
└── 20240115072209_create_posts.rb
2 directories, 1 file
</code></pre></div>
<p>Open up this migration and edit as follows:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># db/migrate/<timestamp>_create_posts.rb</span>
<span class="no">ROM</span><span class="o">::</span><span class="no">SQL</span><span class="p">.</span><span class="nf">migration</span> <span class="k">do</span>
<span class="n">change</span> <span class="k">do</span>
<span class="n">create_table</span> <span class="ss">:posts</span> <span class="k">do</span>
<span class="n">primary_key</span> <span class="ss">:id</span>
<span class="n">column</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">String</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="n">column</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">String</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Then run it with <code>bundle exec rake db:migrate</code>.</p>
<p>Up to this point, we have most of what we need in terms of persistence, and the only thing remaining is an interface to read and write data to the newly created posts table.</p>
<p>In Hanami, this persistence interface is called a <em>relation</em>. Let's build it next.</p>
<h3 id="adding-a-relation">Adding a relation</h3>
<p>Relations provide adapter-specific API interfaces for reading and writing data to the database. You define relations using explicit classes with methods for reading and writing data as you wish it to be done.</p>
<p>The ROM relations <a href="https://rom-rb.org/learn/core/5.2/relations/">documentation</a> provides a wealth of information on the subject, and I encourage the reader to check it out.</p>
<p>Go ahead and create a relation for interacting with the posts table:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># lib/hanami_blog_app/persistence/posts.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Persistence</span>
<span class="k">module</span> <span class="nn">Relations</span>
<span class="k">class</span> <span class="nc">Posts</span> <span class="o"><</span> <span class="no">ROM</span><span class="o">::</span><span class="no">Relation</span><span class="p">[</span><span class="ss">:sql</span><span class="p">]</span>
<span class="n">schema</span><span class="p">(</span><span class="ss">:posts</span><span class="p">,</span> <span class="ss">infer: </span><span class="kp">true</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>A couple of things to note here:</p>
<ul>
<li>The class name <code>Posts</code> will be used to interact with the posts table which is set to <code>:posts</code>.</li>
<li><code>ROM::Relation[:sql]</code> specifies that the <code>rom-sql</code> adapter will be used for this relation.</li>
<li><code>schema(:posts, infer: true)</code>, here the schema will be inferred from the database schema and will include all the posts table columns.</li>
</ul>
<p>As you can see, the relation defines how data will be read and written to the database, but we still need a way to actually fetch and write posts data. Since Hanami leverages the ROM library, we can easily do this using a <em>repository</em>, which we will cover next.</p>
<h3 id="the-post-repository">The post repository</h3>
<p><a href="https://rom-rb.org/learn/repository/5.2/quick-start/">Repositories</a>, also referred to as "repos," provide convenient methods for accessing data from relations. Depending on the set up, a repo can access one or many relations.</p>
<p>Let's set up a repo with some <em>CRUD</em> methods to work with the posts relation.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># lib/hanami_blog_app/persistence/repositories/post.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Persistence</span>
<span class="k">module</span> <span class="nn">Repositories</span>
<span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">ROM</span><span class="o">::</span><span class="no">Repository</span><span class="p">[</span><span class="ss">:posts</span><span class="p">]</span>
<span class="c1"># create, update, delete post</span>
<span class="n">commands</span> <span class="ss">:create</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Here, we define a repo for the posts relation and include a <code>commands</code> macro which defines a <code>Post#create</code> method.</p>
<p>Next, let's add methods for updating and deleting a post.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># lib/hanami_blog_app/persistence/repositories/post.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Persistence</span>
<span class="k">module</span> <span class="nn">Repositories</span>
<span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">ROM</span><span class="o">::</span><span class="no">Repository</span><span class="p">[</span><span class="ss">:posts</span><span class="p">]</span>
<span class="c1"># create, update, delete post</span>
<span class="n">commands</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">update: :by_pk</span><span class="p">,</span> <span class="ss">delete: :by_pk</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p><em>Tip: The <code>by_pk</code> ("by primary key") tells ROM the method is to be applied to a particular post specified by its primary key.</em></p>
<p>Finally, let's add finder methods for fetching all posts and a single post:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># lib/hanami_blog_app/persistence/repositories/post.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Persistence</span>
<span class="k">module</span> <span class="nn">Repositories</span>
<span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">ROM</span><span class="o">::</span><span class="no">Repository</span><span class="p">[</span><span class="ss">:posts</span><span class="p">]</span>
<span class="o">...</span>
<span class="c1"># find all posts</span>
<span class="k">def</span> <span class="nf">all</span>
<span class="n">posts</span><span class="p">.</span><span class="nf">to_a</span>
<span class="k">end</span>
<span class="c1"># find single post</span>
<span class="k">def</span> <span class="nf">find</span><span class="p">(</span><span class="nb">id</span><span class="p">)</span>
<span class="n">posts</span><span class="p">.</span><span class="nf">by_pk</span><span class="p">(</span><span class="nb">id</span><span class="p">).</span><span class="nf">one!</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>And that's it; we've successfully set up the infrastructure for creating and reading posts. However, we're still missing a way for a user to interact with this infrastructure. Let's work on that next.</p>
<h2 id="hanamis-view-layer">Hanami's view layer</h2>
<p>The view layer in a Hanami app is used to render the app's data in HTML, JSON, and other formats. But before we can build any views, it's important to get an overview of a request cycle in a Hanami app.</p>
<p>The diagram below shows how Hanami handles an incoming HTTP request and the parts involved in processing it and sending a response back:</p>
<p><img src="/images/blog/posts/build-hanami-app/simplified-hanami-view-structure.png" alt="Simplified Hanami view structure" /></p>
<p>Let's see how this happens.</p>
<h3 id="hanami-actions">Hanami actions</h3>
<p>When a HTTP request comes in, the request will first encounter an <a href="https://guides.hanamirb.org/v2.1/actions/overview/"><em>action</em></a> which is an individual class that determines how a HTTP request will be handled e.g. what HTTP response to send back, whether to redirect the request and so forth.</p>
<p>An action will have a <code>handle</code> method which takes two arguments:</p>
<ul>
<li><strong>request</strong> - basically an object representing the incoming HTTP request.</li>
<li><strong>response</strong> - an object defining the response back.</li>
</ul>
<p>For example, we can create an action to handle a request for showing a post using the command <code>bundle exec hanami generate action posts.show</code>. This will generate the <code>show</code> action shown below:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># app/actions/posts/create.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Actions</span>
<span class="k">module</span> <span class="nn">Posts</span>
<span class="k">class</span> <span class="nc">Show</span> <span class="o"><</span> <span class="no">HanamiBlogApp</span><span class="o">::</span><span class="no">Action</span>
<span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="o">*</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<span class="n">response</span><span class="p">.</span><span class="nf">body</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">name</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>As well as an accompanying <code>show</code> view and template (which we shall get into a bit later), and append the show route to the routes file:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/routes.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">class</span> <span class="nc">Routes</span> <span class="o"><</span> <span class="no">Hanami</span><span class="o">::</span><span class="no">Routes</span>
<span class="o">...</span>
<span class="n">get</span> <span class="s2">"/posts/:id"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"posts.show"</span><span class="p">,</span> <span class="ss">as: :show_post</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>We can modify our <code>show</code> action to simply respond with the accompanying view while passing it a <code>post_id</code> as the params so that the view will know which post to actually request for, as shown below:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># app/actons/posts/show.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Actions</span>
<span class="k">module</span> <span class="nn">Posts</span>
<span class="k">class</span> <span class="nc">Show</span> <span class="o"><</span> <span class="no">HanamiBlogApp</span><span class="o">::</span><span class="no">Action</span>
<span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<span class="n">response</span><span class="p">.</span><span class="nf">render</span><span class="p">(</span><span class="n">view</span><span class="p">,</span> <span class="ss">id: </span><span class="n">request</span><span class="p">.</span><span class="nf">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>And that's it for the actions; let's move on to the view.</p>
<h3 id="hanami-views">Hanami views</h3>
<p>A Hanami view has two main jobs: decide what data to expose to the template and which template to render. When you generated the <code>show</code> action, an accompanying view and template were also generated (assuming you've installed the <code>hanami-view</code> gem):</p>
<div class="highlight"><pre class="highlight shell"><code>bundle <span class="nb">exec </span>hanami generate action posts.show
Created app/actions/posts/show.rb
Created app/views/posts/show.rb <span class="c"># this is the view</span>
Created app/templates/posts/show.html.erb <span class="c"># this is the template</span>
</code></pre></div>
<p>The show view looks like this:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># app/views/posts/show.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Views</span>
<span class="k">module</span> <span class="nn">Posts</span>
<span class="k">class</span> <span class="nc">Show</span> <span class="o"><</span> <span class="no">Hanami</span><span class="o">::</span><span class="no">View</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Which we can edit as follows:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># app/views/posts/show.rb</span>
<span class="k">module</span> <span class="nn">HanamiBlogApp</span>
<span class="k">module</span> <span class="nn">Views</span>
<span class="k">module</span> <span class="nn">Posts</span>
<span class="k">class</span> <span class="nc">Show</span> <span class="o"><</span> <span class="no">Hanami</span><span class="o">::</span><span class="no">View</span>
<span class="kp">include</span> <span class="no">Deps</span><span class="p">[</span>
<span class="ss">repo: </span><span class="s2">"persistence/repositories/post"</span>
<span class="p">]</span>
<span class="n">expose</span> <span class="ss">:post</span> <span class="k">do</span>
<span class="n">repo</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="nf">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Here's what we just did with the above code:</p>
<ul>
<li>First, we inject a <a href="https://guides.hanamirb.org/v2.0/app/container-and-components/">dependency</a> to include the <code>post</code> repo which we defined earlier, then,</li>
<li>We define an <code>expose</code> block, which returns a post fetched using the repo dependency included in the <code>Deps</code> mixin.</li>
</ul>
<p>Now, we're ready to render the post via a HTML template.</p>
<h3 id="view-templates">View templates</h3>
<p>In Hanami, view templates are responsible for rendering the responses from actions (and views) within HTML, JSON, or other rendering formats.</p>
<p>That said, for our show template to work as expected, we'll first need to define an app template similar to the <code>application.html.erb</code> layout in Rails apps, which will define an app-wide HTML structure.</p>
<p>Create a file <code>app/templates/layouts/app.html.erb</code> and edit it as below:</p>
<div class="highlight"><pre class="highlight erb"><code><span class="c"><!-- app/templates/layouts/app.html.erb --></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">></span>
<span class="nt"><meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width, initial-scale=1.0"</span><span class="nt">></span>
<span class="nt"><title></span>Hanami Blog App<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="cp"><%=</span> <span class="k">yield</span> <span class="cp">%></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div>
<p>And now, edit the <code>show</code> template as shown below:</p>
<div class="highlight"><pre class="highlight erb"><code><span class="c"><!-- app/templates/posts/show.html.erb --></span>
<span class="nt"><h1></span><span class="cp"><%=</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span> <span class="cp">%></span><span class="nt"></h1></span>
<span class="nt"><p></span><span class="cp"><%=</span> <span class="n">post</span><span class="p">.</span><span class="nf">body</span> <span class="cp">%></span><span class="nt"></p></span>
</code></pre></div>
<p>Now run the app with <code>bundle exec hanami server</code>. Assuming everything works as expected, visiting <code>http://localhost:2300/posts/1</code> gives us a post view template similar to the one shown in the screenshot below:</p>
<p><img src="/images/blog/posts/build-hanami-app/hanami-server-running.png" alt="Hanami server running" /></p>
<h2 id="wrapping-up-and-next-steps">Wrapping up and next steps</h2>
<p>In this tutorial, we've taken steps to build a Hanami 2.0 app, starting from gem installation to learning how to handle an incoming HTTP request and responding with views and templates.</p>
<p>Even so, there are several concepts we did not go through in this build, such as taking user data via forms, saving data to the database, catching errors, handling parameters, and so forth. Doing so would make for a very long article; instead, from the foundation set by this tutorial, the reader is encouraged to try and build these features to learn more about this exciting new Ruby framework.</p>
Introducing Honeybadger Insightshttps://www.honeybadger.io/blog/insights/2024-03-07T00:00:00+00:002024-03-19T01:26:39+00:00Benjamin CurtisGain insights into your errors, application logs, and other event streams with a powerful query language and flexible visualizations. It's the logging tool you always wanted and the observability tool you didn't know you needed.<p>I'm pleased to announce a new feature that we've been building for over a year:
<a href="https://www.honeybadger.io/tour/logging-observability/">Honeybadger Insights</a>.
Insights is our take on logging and performance monitoring, helping application
developers gain deeper visibility into what's happening with their applications.
It goes beyond application monitoring and responding to exceptions and downtime.
Insights lets you drill down into the details and step back to see patterns in
your data.</p>
<p><strong>Before we dive in, here's the tl;dr:</strong></p>
<ul>
<li>Insights comes pre-filled with your Honeybadger data, allowing you to query your
errors, uptime checks, and check-ins with a simple <a href="https://docs.honeybadger.io/guides/insights/#querying-and-visualization">query
language</a>
called <em>BadgerQL</em>.</li>
<li>Use BadgerQL to gain <em>immediate</em> insights into your application's health and
performance, then <a href="https://docs.honeybadger.io/guides/insights/#adding-data-from-other-sources">send your application logs and
events</a>
to gain even more visibility.</li>
<li>You can try Insights right now when you sign up for a <a href="/plans/">free Honeybadger
account!</a></li>
</ul>
<h2 id="you-can-query-your-honeybadger-data">You can query your Honeybadger data</h2>
<p>We're already sending all of the error, uptime, and check-in data to Insights,
which we are capturing as part of our monitoring services, so you can slice and
dice your data in any way you want.</p>
<p>Do you want to see all error reports
affecting a particular user, browser, or combination of both? Would you like to
know which browser versions were responsible for the most errors but only for a
specific subdomain or route in your app? Or what times of day had the slowest
response times for your uptime checks over the past week?</p>
<p>You can answer these questions and more with our new query language, <em>BadgerQL</em>.
BadgerQL lets you dive into your Honeybadger data in new and fun ways that
weren't previously possible.</p>
<h2 id="say-hello-to-badgerql">Say Hello to BadgerQL</h2>
<p>BadgerQL is a powerful query language that lets you quickly filter, transform,
and aggregate your data. Here's an example of the default query that's similar
to a SELECT * in SQL:</p>
<div class="highlight"><pre class="highlight sql"><code><span class="n">fields</span> <span class="o">@</span><span class="n">ts</span><span class="p">,</span> <span class="o">@</span><span class="n">preview</span>
<span class="o">|</span> <span class="n">sort</span> <span class="o">@</span><span class="n">ts</span> <span class="k">desc</span>
</code></pre></div>
<p>The default query shows you all the events for a given timeframe, with the most
recent events listed first and a preview of the data for each event.</p>
<p><img src="/images/blog/posts/insights/uptime-event.png" alt="An uptime check event with expanded properties" /></p>
<p>Clicking the disclosure control at the beginning of the row shows you all of the
properties in the event. From there, you can narrow it down to just the
information you need, like the average duration of uptime checks over time:</p>
<div class="highlight"><pre class="highlight sql"><code><span class="n">filter</span> <span class="n">event_type</span><span class="p">::</span><span class="n">str</span> <span class="o">==</span> <span class="nv">"uptime_check"</span>
<span class="o">|</span> <span class="n">stats</span> <span class="k">avg</span><span class="p">(</span><span class="n">duration</span><span class="p">::</span><span class="nb">int</span><span class="p">)</span> <span class="k">by</span> <span class="n">bin</span><span class="p">(</span><span class="mi">4</span><span class="n">h</span><span class="p">)</span>
</code></pre></div>
<p>From there, you can create a chart of the counts:</p>
<p><img src="/images/blog/posts/insights/uptime-check-avg-duration.png" alt="A line chart labeled "End-to-end response time"" /></p>
<p>And add the chart to a dashboard:</p>
<p><img src="/images/blog/posts/insights/dashboard.png" alt="A dashboard of charts, one of which is the same chart from before, labeled "End-to-end response time"" />
<em>It looks like we have some performance optimization work to do!</em></p>
<p>Our <a href="https://docs.honeybadger.io/guides/insights/">documentation</a> explains how
BadgerQL works. Once you start playing with it, we bet you'll have as much fun
as we do devising new queries and creating charts with your query results.</p>
<p>If we stopped there, Insights would be a cool feature. But we didn't stop there. 😁</p>
<h2 id="capture-your-own-structured-events">Capture your own structured events</h2>
<p>All kinds of events are happening in your application right now. New users are
signing up, posting comments, making payments, and so on. With Honeybadger
Insights, it's easy to keep a tab on all those activities, find patterns, and
dive into the details of who did what and when. And it's not just about
metrics—when you can capture extra data about an event, you can do much more
than count how often that event happened. For example, let's say you wanted to
track which posts have received the most comments in the past day. You could
track that event in your Rails app like this:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Comment</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="ss">:post</span>
<span class="n">belongs_to</span> <span class="ss">:author</span>
<span class="n">after_create</span> <span class="ss">:track_event</span>
<span class="c1"># ...</span>
<span class="kp">protected</span>
<span class="k">def</span> <span class="nf">track_event</span>
<span class="no">Honeybadger</span><span class="p">.</span><span class="nf">event</span><span class="p">(</span><span class="s2">"Comment created"</span><span class="p">,</span> <span class="p">{</span>
<span class="ss">comment: </span><span class="n">body</span><span class="p">,</span>
<span class="ss">post: </span><span class="n">post</span><span class="p">.</span><span class="nf">attributes</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="o">*</span><span class="sx">%w(id title created_at)</span><span class="p">),</span>
<span class="ss">author: </span><span class="n">author</span><span class="p">.</span><span class="nf">attributes</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="o">*</span><span class="sx">%w(id name email created_at)</span><span class="p">)</span>
<span class="p">})</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Once you start logging that event, and because you are including some additional
context about the event, you can answer questions like these:</p>
<ul>
<li>How many comments were created in the past day, grouped by hour?</li>
<li>How long after a post was published was the first comment created?</li>
<li>What does the comment distribution for a post look like for the week after the
post was published?</li>
<li>Which users created the most comments?</li>
<li>Which posts had the most engagement?</li>
<li>What comments did that user create?</li>
<li>And more!</li>
</ul>
<p>Once you have the data flowing into Insights, you'll start thinking of more
interesting ways to slice and dice your data, and you'll find that it's helpful
to have extra contextual data around your events.</p>
<h2 id="unleash-badgerql-on-your-application-logs">Unleash BadgerQL on your application logs</h2>
<p>Of course, your logs contain a bunch of interesting data, too. You'll want to
query the number of requests your app is serving, how long those requests are
taking, and other bits of data you're already logging—and often not analyzing.</p>
<p><img src="/images/blog/posts/insights/log-events.png" alt="A list of log events in JSON format" /></p>
<p>Many platforms already log structured events using JSON and logfmt, and if
you're not already logging structured events in your applications, it's easy to get started.</p>
<p>We've made it easy to ingest your log data in Insights, whether it
is currently in <a href="https://docs.honeybadger.io/guides/insights/#log-files">log
files</a>, on cloud
platforms like
<a href="https://docs.honeybadger.io/guides/insights/#heroku-apps">Heroku</a> or
<a href="https://docs.honeybadger.io/guides/insights/#cloudwatch-logs">AWS</a>, or wherever
you can slurp it up and POST it as JSON to our
<a href="https://docs.honeybadger.io/api/reporting-events">API</a>.</p>
<h2 id="what-does-it-cost">What does it cost?</h2>
<p>Insights is too good to lock up exclusively in a paid add-on, so <strong>we're giving
the basics to all Honeybadger users for free!</strong> Use it to explore and
visualize your Honeybadger data: errors, uptime checks, check-ins, and even
notifications.</p>
<p>Insights becomes even more magical when you send more data—structured
application logs, metric events, user behavior, analytics, etc. We include a
base rate of 50MB/day ingest (about 1.5GB/month!) and 7-day retention for free.
If you need more, you can upgrade to one of our <a href="/plans/#price-comparison">paid add-on
plans</a> without changing your current Honeybadger plan
for errors and uptime monitoring.</p>
<h2 id="try-it-now">Try it now!</h2>
<p>Insights is ready and waiting for you to dive in. We already have your
Honeybadger data waiting for you, and we have a free tier that you can use to
start sending in your own data, too. We hope you'll enjoy using it as much as we
do, and this is only the beginning—stay tuned!</p>
How to deploy a Rails app to Renderhttps://www.honeybadger.io/blog/deploy-rails-render/2024-03-05T00:00:00+00:002024-03-19T01:26:39+00:00Jeffery MorhousLearn how to deploy your Ruby on Rails application to Render.com so that you can focus on code instead of infrastructure.<p>There are many ways to deploy a Ruby on Rails application to the internet. Between hosting on your own hardware, renting a virtual machine, using a cloud provider, and using a platform, the opportunities are endless. The low-hassle way to host a Rails application is to use a Platform as a Service (PaaS). In this article, we'll show you how to deploy a Rails Application to <a href="https://render.com/">Render.com</a>, and as a bonus, monitor it with Honeybadger! You can find the final project <a href="https://github.com/JeffMorhous/event-tracker">here on Github</a>.</p>
<p>Going a step further than cloud hosting and using a platform to deploy your web apps provides incredible convenience at a cost. Under the hood, platforms such as Render and <a href="https://www.heroku.com/">Heroku</a> use <a href="https://aws.amazon.com/">Amazon Web Services</a> or other cloud providers. Using a platform will cost you more than using a cloud provider directly, as the service is marked up. For the extra cost, you'll be able to deploy and scale a functional service without needing to understand or manage the underlying infrastructure. You'll also gain features like replication, autoscaling, and preview deployments without extra setup. Cloud providers are complicated enough to have built an ecosystem around education and certification, while platforms like Render allow developers to deploy an application in a single sitting.</p>
<h2 id="setting-up-an-example-ruby-on-rails-application">Setting up an example Ruby on Rails application</h2>
<p>If you're reading this, you likely already have a Rails app you'd like to deploy. In case you don't, we'll quickly go over creating a Rails app in this section.</p>
<p>If you don't already have rails installed, install version 7.1.1 with:</p>
<div class="highlight"><pre class="highlight shell"><code>gem <span class="nb">install </span>rails <span class="nt">-v</span> 7.1.1
</code></pre></div>
<p>First, create a new Rails app using the <code>rails new</code> command. We'll pass it a specific version:</p>
<div class="highlight"><pre class="highlight shell"><code>rails _7.1.1_ new appname
</code></pre></div>
<p>Next, go into the directory that was created:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">cd </span>appname
</code></pre></div>
<p>Next, generate a new model and controller using <a href="https://guides.rubyonrails.org/v3.2.21/getting_started.html#getting-up-and-running-quickly-with-scaffolding">the scaffold generator</a>. For this example, we'll model "Event":</p>
<div class="highlight"><pre class="highlight shell"><code>rails generate scaffold Event title:string <span class="nb">date</span>:datetime
</code></pre></div>
<p>This creates a number of files to support Creating, Reading, Updating, and Deleting <code>Event</code> models in a database. We'll run the created database migration to create the necessary <code>Events</code> table next with:</p>
<div class="highlight"><pre class="highlight shell"><code>rails db:migrate
</code></pre></div>
<p>The scaffold already generated basic views, models, and controllers to support CRUD actions on events. To see the code in action, run the rails server with:</p>
<div class="highlight"><pre class="highlight shell"><code>rails server
</code></pre></div>
<p>Now, visit <code>localhost:3000/events</code> in your browser. You'll see a simple white page with an "Events" header and a "New event" button.</p>
<p><img src="/images/blog/posts/deploy-rails-render/events-index-page.png" title="Events Index Page" alt="A screenshot of the Events Index Page" />
<em>A screenshot of the Events Index Page</em></p>
<p>Clicking the button will take you to a form for creating the event, and creating the event will persist it in the database. You can also delete and edit an existing event, all from just the scaffold! This functional CRUD app will serve as a great example for our deployment. Push up the repository to Github as a final step.</p>
<h2 id="signing-up-for-render">Signing up for Render</h2>
<p><a href="https://dashboard.render.com/login">Signing up for Render</a> is easy. Signing up via Github will save you a step later, as we'll connect to Github to deploy our app. Render's free tier is generous, particularly for folks missing Heroku's free tier.</p>
<p>You can configure your service in the UI, or configure it in your codebase, using infrastructure as code. Many in the DevOps industry prefer infrastructure as code whenever possible, so we'll do that in this example.</p>
<h2 id="configuring-your-deployment-with-deploy-yaml-and-build-sh">Configuring your deployment with <code>deploy.yaml</code> and <code>build.sh</code></h2>
<p>Add a file to the root of your repository called <code>render.yaml</code>. In that file, paste this:</p>
<div class="highlight"><pre class="highlight yaml"><code><span class="na">databases</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">name_of_your_app</span>
<span class="na">databaseName</span><span class="pi">:</span> <span class="s">name_of_your_app</span>
<span class="na">user</span><span class="pi">:</span> <span class="s">name_of_your_app</span>
<span class="na">plan</span><span class="pi">:</span> <span class="s">free</span>
<span class="na">services</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">web</span>
<span class="na">plan</span><span class="pi">:</span> <span class="s">free</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">name_of_your_app</span>
<span class="na">env</span><span class="pi">:</span> <span class="s">ruby</span>
<span class="na">buildCommand</span><span class="pi">:</span> <span class="s2">"</span><span class="s">./bin/render-build.sh"</span>
<span class="na">startCommand</span><span class="pi">:</span> <span class="s2">"</span><span class="s">bundle</span><span class="nv"> </span><span class="s">exec</span><span class="nv"> </span><span class="s">puma</span><span class="nv"> </span><span class="s">-C</span><span class="nv"> </span><span class="s">config/puma.rb"</span>
<span class="na">envVars</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s">DATABASE_URL</span>
<span class="na">fromDatabase</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">name_of_your_app</span>
<span class="na">property</span><span class="pi">:</span> <span class="s">connectionString</span>
<span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s">RAILS_MASTER_KEY</span>
<span class="na">sync</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div>
<p>In this file, replace <code>name_of_your_app</code> in all places with the name of your app. In our example, we'll call it event_tracker. This specifies that you want a web service and database on the free plan. You'll notice that the file references a build command in a file called <code>bin/render-build.sh</code>, which doesn't exist yet. Create it, and paste the following:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># exit on error</span>
<span class="nb">set</span> <span class="nt">-o</span> errexit
bundle <span class="nb">install
</span>bundle <span class="nb">exec </span>rake assets:precompile
bundle <span class="nb">exec </span>rake assets:clean
bundle <span class="nb">exec </span>rake db:migrate
</code></pre></div>
<p>This file will execute when your service starts, so they should go here if you need any other custom build steps.</p>
<h2 id="configuring-postgres-as-a-database-adapter">Configuring Postgres as a database adapter</h2>
<p>Rails applications begin using SQLite unless specified otherwise when generated. Render requires Postgres, so if your application doesn't yet have it, you'll need to add it.</p>
<p>In your Gemfile, add <code>gem "pg"</code> to the main section of dependencies. Separately, move
<code>gem "sqlite3", "~> 1.4"</code> into <code>group: development</code>.</p>
<p>Finally, run a <code>bundle install</code> to update Gemfile.lock.</p>
<p>You must also change your database configuration in <code>database.yml</code> to specify that Postgres should be used in production. Your <code>database.yml</code> should now look like this:</p>
<div class="highlight"><pre class="highlight yaml"><code><span class="na">default</span><span class="pi">:</span> <span class="nl">&default</span>
<span class="na">adapter</span><span class="pi">:</span> <span class="s">postgresql</span>
<span class="na">pool</span><span class="pi">:</span> <span class="s"><%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %></span>
<span class="na">timeout</span><span class="pi">:</span> <span class="m">5000</span>
<span class="na">development</span><span class="pi">:</span>
<span class="na"><<</span><span class="pi">:</span> <span class="nv">*default</span>
<span class="na">adapter</span><span class="pi">:</span> <span class="s">sqlite3</span>
<span class="na">database</span><span class="pi">:</span> <span class="s">db/development.sqlite3</span>
<span class="c1"># Warning: The database defined as "test" will be erased and</span>
<span class="c1"># re-generated from your development database when you run "rake".</span>
<span class="c1"># Do not set this db to the same as development or production.</span>
<span class="na">test</span><span class="pi">:</span>
<span class="na"><<</span><span class="pi">:</span> <span class="nv">*default</span>
<span class="na">database</span><span class="pi">:</span> <span class="s">db/test.sqlite3</span>
<span class="na">production</span><span class="pi">:</span>
<span class="na"><<</span><span class="pi">:</span> <span class="nv">*default</span>
<span class="na">database</span><span class="pi">:</span> <span class="s">event_tracker_production</span>
<span class="na">username</span><span class="pi">:</span> <span class="s">event_tracker</span>
<span class="na">password</span><span class="pi">:</span> <span class="s"><%= ENV['EVENT_TRACKER_DATABASE_PASSWORD'] %></span>
</code></pre></div>
<p>You will need to swap out the value for the production database key, <code>event_tracker_production</code> with the name of your app, appended by <code>production</code>. You'll also need to make the username, <code>event_tracker</code>, the name of your application. The environment variable for the production password should have the name of your app as <code>APP_NAME_PASSWORD</code>. Render will automatically set that environment variable with the appropriate value.</p>
<h2 id="deploying-with-the-render-dashboard">Deploying with the Render dashboard</h2>
<p>Once you have pushed all your app changes to GitHub, it's time to create your service in the Render dashboard. On <a href="https://dashboard.render.com/blueprints">the dashboard, select Blueprints</a>, then "New Blueprint Instance." This will allow us to create a new service using our render.yml file.</p>
<p>Select the repository of the application you would like to deploy and give your blueprint a name. You'll also need to paste in your Rails Master Key, which you can get from the file in your repository (which is not checked into git) titled <code>master.key</code>.</p>
<p><img src="/images/blog/posts/deploy-rails-render/render-new-blueprint.png" title="Render dashboard" alt="A screenshot of the Render dashboard" />
<em>A screenshot of the Render dashboard</em></p>
<p>Paste in your master key, and let Render work its magic! In a few moments, your Rails application and its database will be live on the web. By default, Render will auto-deploy the service anytime a new commit is made to your default branch. Higher tiers of Render allow for more customization, like preview deployments.</p>
<h2 id="adding-honeybadger-for-monitoring">Adding Honeybadger for monitoring</h2>
<p>Honeybadger lets you quickly add application health monitoring to your app with minimal setup. After creating an account, add a new project in the dashboard.</p>
<p><img src="/images/blog/posts/deploy-rails-render/honeybadger-setup.png" title="Honeybadger dashboard" alt="A screenshot of the Honeybadger dashboard" />
<em>A screenshot of the Honeybadger dashboard</em></p>
<p>After selecting your framework (Rails, in this case), you'll be met with some instructions for setting it up.</p>
<p>First, add the honeybadger gem by running:</p>
<div class="highlight"><pre class="highlight shell"><code>bundle add honeybadger
</code></pre></div>
<p>Next, run the provided command to generate Honeybadger's configuration files and send a test alert.</p>
<p>Check in your changes and push them to GitHub, and Render will automatically deploy your application with Honeybadger!</p>
<p>To demonstrate the error monitoring, we can write a bug on purpose. For example, I can change my example application's <code>events_controller.rb</code> to have an index method that references a class that I haven't yet written:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">index</span>
<span class="vi">@events</span> <span class="o">=</span> <span class="no">Birthday</span><span class="p">.</span><span class="nf">all</span>
<span class="k">end</span>
</code></pre></div>
<p>Committing this and pushing it will auto-deploy the faulty code. After waiting a few minutes for the deploy, we can visit our application's URL and append <code>/events</code> to visit the events index page that contains the bug. Bonus points if you can guess the Ruby error message!</p>
<p>Visiting the <code>/events</code> route throws an error, which is promptly reported to Honeybadger. The error, stack trace, URL, and other helpful information are easily accessible in Honeybadger.</p>
<p><img src="/images/blog/posts/deploy-rails-render/honeybadger-error.png" title="Honeybadger error page" alt="A screenshot of the Honeybadger error page" />
<em>A screenshot of the Honeybadger error page</em></p>
<h2 id="conclusion">Conclusion</h2>
<p>In conclusion, deploying a Ruby on Rails application using Render offers a straightforward and efficient solution for developers looking to streamline their deployment process. This guide has walked you through setting up a basic Rails app, configuring it for deployment on Render, and integrating Honeybadger for monitoring. The process, though involved, is simplified by the platform's user-friendly interface compared to cloud providers. As the web development landscape continues to evolve, tools like these help developers focus more on creating and less on the complexities of deployment and maintenance.</p>
File operations in Node.jshttps://www.honeybadger.io/blog/file-operations-node/2024-02-27T00:00:00+00:002024-03-19T01:26:39+00:00Samson OmojolaExplore the intricacies of reading and writing files in various formats, handling directories, mastering error-handling techniques, and harnessing the efficiency of streams for powerful, performance-boosting file operations.<p>File operations are fundamental when working with data in Node.js. Node.js provides a built-in fs module that allows us to perform various file operations. Whether it's reading from or writing to files, handling different file formats, or managing directories, Node.js offers versatile functionalities to interact with the file system.</p>
<h2 id="reading-files-in-different-formats">Reading files in different formats</h2>
<p>Let's dive into different file formats and their corresponding reading methods to effectively handle diverse data structures.</p>
<h3 id="reading-text-files">Reading text files</h3>
<p>Text files are a common way to store information. Let's read the content of a text file named 'example.txt':</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">example.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Text file content:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We're using <code>fs.readFile()</code> to read the 'example.txt' file.
The 'utf8' argument specifies the file's character encoding.
The anonymous function processes the file content after it's read.
If an error occurs, it's captured and logged to the console.</p>
<h3 id="reading-json-files">Reading JSON files</h3>
<p>JSON files, great for structured data storage, can be easily converted into JavaScript objects:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">data.json</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">jsonData</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">JSON content:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">jsonData</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>The <code>fs.readFile()</code> function is used here to read 'data.json'.
In the callback, if no errors are encountered, the file content is parsed using <code>JSON.parse()</code> to transform it into a JavaScript object.
The resultant object (jsonData) is then logged to the console.</p>
<h3 id="reading-csv-files">Reading CSV files</h3>
<p>Parsing CSV files often requires specialized libraries. Here's an example using <code>csv-parser</code>.</p>
<p>First, install the package:</p>
<div class="highlight"><pre class="highlight shell"><code>npm <span class="nb">install </span>csv-parser
</code></pre></div>
<p>Then, parse the CSV file:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">csv</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">csv-parser</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="dl">'</span><span class="s1">data.csv</span><span class="dl">'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">pipe</span><span class="p">(</span><span class="nx">csv</span><span class="p">())</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">data</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">row</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">CSV Row:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">row</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">end</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">CSV file processing complete</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>The <code>csv-parser</code> library is used to handle the parsing of the CSV file. <code>fs.createReadStream()</code> initiates a readable stream for 'data.csv'. The <code>.pipe(csv())</code> method processes the stream, and for each row, the (row) => {} function logs the row to the console.
The .on('end') function executes after the CSV file has been fully processed.</p>
<h3 id="reading-binary-files">Reading binary files</h3>
<p>Binary files, containing data in a binary format, can be read as buffer data:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">binaryFile.bin</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Binary data:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>Using <code>fs.readFile()</code>, the content of 'binaryFile.bin' is read.
In there are no errors, the data are retrieved as a buffer and displayed in the console.</p>
<h2 id="writing-files-in-different-formats">Writing files in different formats</h2>
<p>Having explored reading files in various formats, let's venture into writing data to different file formats in Node.js. Understanding these methods is crucial for manipulating and storing information across diverse file types.</p>
<h3 id="writing-text-files">Writing text files</h3>
<p>To commence, let's initiate writing content to a text file named 'newFile.txt':</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">This is the content we're writing to the text file.</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">writeFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">newFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Text file written successfully</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>Here, we utilize <code>fs.writeFile()</code> to create a new text file, 'newFile.txt'. The content variable contains the data we want to write to the file. We specify the file encoding as 'utf8'.
Upon completion, we log a success message to the console. Errors encountered during the process are also logged.</p>
<h3 id="writing-json-files">Writing JSON files</h3>
<p>Moving on to writing data in a JSON format, let's create a new JSON file, 'newData.json':</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">key</span><span class="p">:</span> <span class="dl">'</span><span class="s1">value</span><span class="dl">'</span><span class="p">,</span>
<span class="na">number</span><span class="p">:</span> <span class="mi">42</span>
<span class="p">};</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">writeFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">newData.json</span><span class="dl">'</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">JSON file written successfully</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We utilize <code>fs.writeFile()</code> to generate a new JSON file, 'newData.json'. The data object contains the information we want to store in the file, serialized using <code>JSON.stringify()</code>. We specify the file encoding as 'utf8'. Upon successful completion, we log a success message. Errors are logged to the console.</p>
<p>Writing CSV files CSV files can also be generated using specific methods. Let's
install the 'fast-csv' package:</p>
<div class="highlight"><pre class="highlight shell"><code>npm <span class="nb">install </span>fast-csv
</code></pre></div>
<p>Next, create a CSV file, 'newData.csv', using 'fast-csv':</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">csvWriter</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fast-csv</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Alice</span><span class="dl">'</span><span class="p">,</span> <span class="na">age</span><span class="p">:</span> <span class="mi">28</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Bob</span><span class="dl">'</span><span class="p">,</span> <span class="na">age</span><span class="p">:</span> <span class="mi">32</span> <span class="p">}</span>
<span class="p">];</span>
<span class="nx">csvWriter</span><span class="p">.</span><span class="nx">writeToPath</span><span class="p">(</span><span class="dl">'</span><span class="s1">newData.csv</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">,</span> <span class="p">{</span> <span class="na">headers</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">finish</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">CSV file written successfully</span><span class="dl">'</span><span class="p">));</span>
</code></pre></div>
<p>Here, we employ 'fast-csv' to create a new CSV file, 'newData.csv'.
The data array holds the information to be written to the CSV file, including column headers. Upon successful completion, a message indicating file-writing success is logged.</p>
<h3 id="writing-binary-files">Writing binary files</h3>
<p>Binary files, storing data in a binary format, can also be generated. Let's write to a binary file, 'newBinaryFile.bin':</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">bufferData</span> <span class="o">=</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="dl">'</span><span class="s1">Hello, this is binary data.</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">writeFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">newBinaryFile.bin</span><span class="dl">'</span><span class="p">,</span> <span class="nx">bufferData</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Binary file written successfully</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>Using <code>fs.writeFile()</code>, we create a new binary file, 'newBinaryFile.bin'.
The bufferData variable stores binary information encoded as a buffer.
After successful completion, a success message is logged. Any encountered errors are also handled and logged.</p>
<h2 id="synchronous-and-asynchronous-file-operations">Synchronous and asynchronous file operations</h2>
<p>Understanding synchronous and asynchronous operations is vital when working with files in Node.js.</p>
<h3 id="synchronous-file-operations">Synchronous file operations</h3>
<p>In synchronous file operations, code execution waits for the file operation to complete before moving on to the next task. Here's an example of synchronous file reading:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">example.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Synchronous Read:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>We use <code>fs.readFileSync()</code> to synchronously read the content of 'example.txt'. The code waits until the file is either read completely or an error occurs. If successful, the retrieved data is logged via console.log. Errors encountered during the process are captured and logged.</p>
<p>Asynchronous file operations allow the code to continue executing while the file operation occurs in the background. Here's an example of asynchronous file reading:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">example.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Asynchronous Read:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We use fs.<code>readFile()</code> for asynchronous reading of 'example.txt'.
The code continues executing without waiting for the file operation to complete. Upon completion, the data are logged via console.log. Errors, if encountered, are handled and logged.</p>
<h3 id="comparison">Comparison</h3>
<p>Synchronous operations are straightforward but can block the execution thread, potentially leading to slower performance.
Asynchronous operations enhance efficiency by allowing other tasks to run simultaneously, making them preferable for I/O operations.</p>
<h2 id="writing-to-files-synchronously-and-asynchronously">Writing to files synchronously and asynchronously</h2>
<p>In Node.js, writing data to files can be achieved both synchronously and asynchronously.</p>
<h3 id="writing-to-files-synchronously">Writing to files synchronously</h3>
<p>Synchronous file writing ensures that code execution halts until the file operation is completed. Here's an example of synchronous file writing:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Content to be written synchronously.</span><span class="dl">"</span><span class="p">;</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">writeFileSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">syncFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">File written synchronously.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error writing file synchronously:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>We use fs.<code>writeFileSync()</code> to synchronously write content to 'syncFile.txt'.
The try...catch block captures errors during the writing process.
Upon successful completion, a success message is logged. Errors are also logged.</p>
<h3 id="writing-to-files-asynchronously">Writing to files asynchronously</h3>
<p>Asynchronous file writing allows the code to continue executing while the file operation happens in the background. Here's an example of asynchronous file writing:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Content to be written asynchronously.</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">writeFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">asyncFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error writing file asynchronously:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">File written asynchronously.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We use <code>fs.writeFile()</code> to asynchronously write content to 'asyncFile.txt'. The code continues to execute without waiting for the write operation to complete. If it works, a success message is logged. Errors encountered during the process are handled and logged.</p>
<h2 id="appending-data-to-files">Appending data to files</h2>
<p>In Node.js, appending data to existing files allows us to add new content without overwriting the existing file content. Let's explore how to append data to files using Node.js.</p>
<p>To append data to a file, you can use the <code>fs.appendFile()</code> method in Node.js. Here's an example:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">additionalContent</span> <span class="o">=</span> <span class="dl">"</span><span class="se">\n</span><span class="s2">Additional content to append.</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">appendFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">existingFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="nx">additionalContent</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error appending data:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Data appended to file.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We employ <code>fs.appendFile()</code> to add content to 'existingFile.txt'.
The additionalContent variable contains the data to append. The specified encoding is 'utf8'.</p>
<p>Appending data to files in Node.js allows you to seamlessly add new content without rewriting the entire file. This method is especially useful for log files, continuous data updates, and maintaining historical information.</p>
<h2 id="renaming-and-deleting-files">Renaming and deleting files</h2>
<p>Renaming and deleting files are common file management tasks in Node.js. Let's explore how to rename and delete files.</p>
<h3 id="renaming-files">Renaming files</h3>
<p>To rename a file in Node.js, we can use the <code>fs.rename()</code> method. Here's an example:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">rename</span><span class="p">(</span><span class="dl">'</span><span class="s1">oldFileName.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">newFileName.txt</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error renaming file:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">File renamed successfully.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We utilize <code>fs.rename()</code> to rename 'oldFileName.txt' to 'newFileName.txt'. The method takes the old file name, the new file name, and a callback function.</p>
<h3 id="deleting-files">Deleting files</h3>
<p>Deleting files in Node.js is straightforward using the <code>fs.unlink()</code> method. Here's an example:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">unlink</span><span class="p">(</span><span class="dl">'</span><span class="s1">fileToDelete.txt</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error deleting file:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">File deleted successfully.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We use <code>fs.unlink()</code> to delete the file 'fileToDelete.txt'.
The method takes the file name and a callback function.</p>
<h2 id="working-with-directories">Working with directories</h2>
<p>Let's explore how to perform various tasks, such as creating, reading, and removing directories.</p>
<h3 id="creating-directories">Creating directories</h3>
<p>To create directories in Node.js, we can use the <code>fs.mkdir()</code> method. Here's an example:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">mkdir</span><span class="p">(</span><span class="dl">'</span><span class="s1">newDirectory</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">recursive</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error creating directory:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Directory created successfully.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We utilize <code>fs.mkdir()</code> to create a new directory named 'newDirectory'. The { recursive: true } option enables the creation of nested directories if they don't exist.</p>
<h3 id="reading-directories">Reading directories</h3>
<p>Reading directory contents in Node.js can be done using the <code>fs.readdir()</code> method. Here's an example:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">readdir</span><span class="p">(</span><span class="dl">'</span><span class="s1">directoryPath</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">files</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error reading directory:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Directory contents:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">files</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We use <code>fs.readdir()</code> to read the contents of a directory ('directoryPath'). The method fetches the list of files in the specified directory. Upon successful retrieval, the list of directory contents is logged. Errors are captured and logged.</p>
<h3 id="removing-directories">Removing directories</h3>
<p>Deleting directories in Node.js can be performed using the <code>fs.rmdir()</code> method. Here's an example:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">rm</span><span class="p">(</span><span class="dl">'</span><span class="s1">directoryToDelete</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">recursive</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error deleting directory:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Directory deleted successfully.</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We use <code>fs.rmdir()</code> to delete the 'directoryToDelete'. The { recursive: true } option enables the removal of the directory and its contents. Upon successful deletion, a confirmation message is logged.</p>
<h2 id="handling-file-system-errors">Handling file system errors</h2>
<p>Effective error handling is essential when working with file operations in Node.js. Let's explore techniques used to efficiently manage file-system errors.</p>
<h3 id="error-handling-best-practices">Error handling best practices</h3>
<p>In Node.js, handling file-system errors involves implementing strategies to anticipate and manage potential errors that might occur during file operations. Here's a basic example:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">nonExistentFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">code</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">ENOENT</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">File not found:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error reading file:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">File content:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<p>We use <code>fs.readFile()</code> to read 'nonExistentFile.txt'. The conditional checks for specific error codes. In this case, if the error code is 'ENOENT' (indicating the file doesn't exist), it's handled separately. Other errors are handled with a generic error message.</p>
<h3 id="implementing-robust-error-handling">Implementing robust error handling</h3>
<p>In more complex applications, handling errors robustly is crucial. Implementing try-catch blocks and appropriate error messages helps maintain application stability:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">someFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">File content:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">code</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">ENOENT</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">File not found:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error reading file:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>We utilize a try-catch block to read the file 'someFile.txt'.
The catch block includes conditional error handling based on specific error codes, providing detailed error messages based on the error type.</p>
<h2 id="using-streams-for-efficient-file-operations">Using streams for efficient file operations</h2>
<p>Streams in Node.js offer a powerful way to handle data, increasing efficiency in file operations by processing data in chunks.</p>
<h3 id="understanding-streams">Understanding streams</h3>
<p>In Node.js, streams are objects designed to handle I/O operations efficiently by breaking data into smaller chunks, which are then processed sequentially. There are various types of streams, including readable, writable, duplex, and transform.</p>
<h3 id="advantages-of-streams">Advantages of Streams</h3>
<p>Streams offer several advantages for file operations in Node.js:</p>
<ol>
<li>Memory Efficiency: Streams process data in chunks, minimizing the memory footprint, which is especially beneficial for large files.</li>
<li>Improved Performance: Since data are processed in chunks, they can be handled more quickly and efficiently.</li>
<li>Piping Operations: Streams can be easily connected (piped) together to seamlessly pass data from one operation to another.</li>
</ol>
<h3 id="reading-and-writing-files-with-streams">Reading and writing files with streams</h3>
<p>Let's consider an example of reading a file using a readable stream and writing its content to another file using a writable stream:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">readableStream</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="dl">'</span><span class="s1">inputFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">writableStream</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">createWriteStream</span><span class="p">(</span><span class="dl">'</span><span class="s1">outputFile.txt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf8</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">readableStream</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span><span class="nx">writableStream</span><span class="p">);</span>
</code></pre></div>
<p>We create a readable stream (readableStream) to read 'inputFile.txt' and a writable stream (writableStream) to write to 'outputFile.txt'.
The readableStream.pipe(writableStream) operation pipes the data from the readable stream to the writable stream, efficiently writing the content from the input file to the output file.</p>
<p>Utilizing streams is a powerful technique in Node.js for handling file operations, enhancing performance and memory efficiency.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In Node.js, file operations are fundamental for managing data, handling I/O tasks efficiently, and building robust applications. In this comprehensive guide, we have explored the intricacies of file operations, delving into various facets of working with files and directories, handling errors, and leveraging streams for enhanced performance.
Whether you're a seasoned developer or just starting your journey with Node.js, leveraging the knowledge shared in this guide will undoubtedly aid in the creation of powerful and efficient applications. Happy coding!</p>
How to dockerize a Django, Preact, and PostgreSQL Applicationhttps://www.honeybadger.io/blog/dockerize-django-preact-postgres/2024-02-20T00:00:00+00:002024-03-19T01:26:39+00:00Charlie MacnamaraDockerizing your Django application can be intimidating, but the rewards outweigh the risks. In this guide, Charlie Macnamara walks you through the setup process so you can get the most out of your applications.<p>During my recent deep dive into new technologies, I found the classic issues of integrating numerous tech tools effectively. I've written about my experiences to save you the trouble I had.</p>
<p>One essential component I've looked into is using Docker to implement containerization. While the initial setup takes a little longer, it significantly simplifies and optimizes your technological stack.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, make sure you have the following:</p>
<ul>
<li><a href="https://docs.docker.com/get-docker/">Docker</a></li>
<li><a href="https://docs.docker.com/compose/install/">Docker Compose</a></li>
<li><a href="https://www.djangoproject.com/download/">Django</a></li>
<li><a href="https://nodejs.org/en">Node.js</a></li>
<li><a href="https://www.npmjs.com">npm</a></li>
</ul>
<p>We'll start a Django application and then a Preact application, containerize them both, run the containers, and then create an API to ensure the stack works correctly.</p>
<h2 id="getting-started">Getting started</h2>
<p>To start, create an empty folder; we'll name ours <code>django-preact-docker</code> and navigate to this folder from the terminal.</p>
<p>We'll follow the first steps of creating a Django application - creating a virtual environment, activating it, and then installing Django.</p>
<p>(If <code>virtualenv</code> is not installed, run <code>pip install virtualenv</code>)</p>
<p>Run these commands in the terminal:</p>
<div class="highlight"><pre class="highlight shell"><code>virtualenv venv
<span class="nb">source</span> ./venv/bin/activate
</code></pre></div>
<p>Then, with our virtual environment running, install Django along with some other dependencies:</p>
<div class="highlight"><pre class="highlight shell"><code>pip <span class="nb">install </span>django django-cors-headers psycopg2-binary
</code></pre></div>
<p>Now, we can create a new Django project and get it running on our local server.</p>
<h3 id="setting-up-django">Setting Up Django</h3>
<p>From the terminal, navigate to <code>django-preact-docker</code>, and run:</p>
<div class="highlight"><pre class="highlight shell"><code>django-admin startproject backend
</code></pre></div>
<p>The above command creates a new folder, <code>backend</code>. Navigate into this folder:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">cd </span>backend
</code></pre></div>
<p>And then start the project:</p>
<div class="highlight"><pre class="highlight shell"><code>python manage.py runserver
</code></pre></div>
<p>You can test if the server is running by going to: <a href="http://127.0.0.1:8000/">http://127.0.0.1:8000/</a></p>
<p><img src="/images/blog/posts/dockerize-django-preact-postgres/test_server.png" alt="a screenshot showing the successful test server" /></p>
<p>Finally, create a requirements file that will hold our requirements for the project. While in <code>django-preact-docker/backend</code>, run:</p>
<div class="highlight"><pre class="highlight shell"><code>pip freeze <span class="o">></span> requirements.txt
</code></pre></div>
<h3 id="setting-up-our-preact-application">Setting up our Preact application</h3>
<p>Head back to our root folder <code>django-preact-docker</code>.</p>
<p>In the terminal, run:</p>
<div class="highlight"><pre class="highlight shell"><code>npm init preact
</code></pre></div>
<p>When prompted, change the project directory to "frontend," then hit enter for the rest of the options.</p>
<p>Once installed, from the terminal, navigate into <code>django-preact-docker/frontend</code> and run the following command to set up a development server:</p>
<div class="highlight"><pre class="highlight shell"><code>npm run dev
</code></pre></div>
<p>Once the server has started, it will print a local development URL to open in your browser. Check if this is working before moving on!</p>
<p><img src="/images/blog/posts/dockerize-django-preact-postgres/test_preact.png" alt="a screenshot showing a successful precat response" /></p>
<h2 id="containerizing">Containerizing</h2>
<p>Next, we must create configuration files, so Docker knows what to do.</p>
<h3 id="containerize-django">Containerize Django</h3>
<p>Navigate to <code>django-preact-docker/backend</code> and create a new file:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">touch</span> .dockerignore
</code></pre></div>
<p>Open the file and add the following:</p>
<div class="highlight"><pre class="highlight plaintext"><code># The file may be hidden on Finder; the default macOS shortcut is Shift-Command-. to show hidden files
venv
env
.env
Dockerfile
</code></pre></div>
<p>Next, run:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">touch </span>Dockerfile
</code></pre></div>
<p>Open it and add:</p>
<div class="highlight"><pre class="highlight docker"><code><span class="k">FROM</span><span class="s"> python:3.8-alpine</span>
<span class="k">ENV</span><span class="s"> PYTHONUNBUFFERED 1</span>
<span class="k">WORKDIR</span><span class="s"> /app/backend</span>
<span class="k">COPY</span><span class="s"> requirements.txt /app/backend/</span>
<span class="k">RUN </span>apk add <span class="nt">--update</span> <span class="nt">--no-cache</span> postgresql-dev gcc python3-dev musl-dev
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">CMD</span><span class="s"> [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]</span>
</code></pre></div>
<p>In the above code:</p>
<ul>
<li><code>FROM</code> specifies the parent image that we'll be using</li>
<li><code>ENV</code> sets the environment variable <code>PYTHONUNBUFFERED</code> to "1" to give us real-time log output.</li>
<li><code>WORKDIR</code> specifies the working directory within the container.</li>
<li><code>COPY</code> the requirements file to the working directory and later install the requirements.</li>
<li><code>RUN</code>, install some more dependencies for psycopg-2,</li>
<li><code>COPY</code> the content of our backend to the Docker container</li>
<li>The starting command for our container</li>
</ul>
<h3 id="containerize-preact">Containerize Preact</h3>
<p>Go to <code>django-preact-docker/frontend</code> and create a new file:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">touch</span> .dockerignore
</code></pre></div>
<p>Open the file in your text editor and add the following:</p>
<div class="highlight"><pre class="highlight plaintext"><code>node_modules
npm-debug.log
Dockerfile
yarn-error.log
</code></pre></div>
<p>Go back to the terminal in <code>frontend</code> and create another Dockerfile:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">touch </span>Dockerfile
</code></pre></div>
<p>Open and edit "Dockerfile" to contain:</p>
<div class="highlight"><pre class="highlight docker"><code><span class="k">FROM</span><span class="s"> node:16-alpine</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> package*.json ./</span>
<span class="k">RUN </span>npm <span class="nb">install</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">EXPOSE</span><span class="s"> 5173</span>
<span class="k">CMD</span><span class="s"> ["npm", "run", "dev"]</span>
</code></pre></div>
<p>Lastly, we must add the new <code>npm run dev</code> script. Head to <code>frontend/package.json</code> and change <code>scripts</code> too:</p>
<div class="highlight"><pre class="highlight json"><code><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vite --host 0.0.0.0"</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>
<h2 id="packaging-our-applications-with-docker-compose">Packaging our applications with Docker Compose</h2>
<p>Next, we must create a configuration file to run our two Docker containers together. In the main folder, <code>django-preact-docker</code>, create a new file called <code>docker-compose.yml</code>:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nb">touch </span>docker-compose.yml
</code></pre></div>
<p>Open it and edit it to contain the following:</p>
<div class="highlight"><pre class="highlight yaml"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.9'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">db</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">postgres:14-alpine</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">5432:5432'</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">POSTGRES_PASSWORD=postgres</span>
<span class="pi">-</span> <span class="s">POSTGRES_USER=postgres</span>
<span class="pi">-</span> <span class="s">POSTGRES_DB=postgres</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./data/db:/var/lib/postgresql/data/</span>
<span class="na">frontend</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">./frontend</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">5173:5173'</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./frontend:/app/frontend</span>
<span class="na">depends_on</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">backend</span>
<span class="na">backend</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">./backend</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">POSTGRES_PASSWORD=postgres</span>
<span class="pi">-</span> <span class="s">POSTGRES_USER=postgres</span>
<span class="pi">-</span> <span class="s">POSTGRES_DB=postgres</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">8000:8000'</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./backend:/app/backend</span>
<span class="na">depends_on</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">db</span>
</code></pre></div>
<p>The Docker Compose file tells Docker how the different containers work together.</p>
<h2 id="build-the-containers">Build the containers</h2>
<p>Go to the root <code>django-preact-docker</code> from the terminal and run the following command (this may take a while):</p>
<div class="highlight"><pre class="highlight shell"><code>docker-compose build
</code></pre></div>
<p>Once complete, you should see the images in Docker Desktop.</p>
<p>You can then run the containers with the following:</p>
<div class="highlight"><pre class="highlight shell"><code>docker-compose up
</code></pre></div>
<p>After this, the servers are accessible at the ports:</p>
<ul>
<li>5173 for frontend</li>
<li>8000 for backend</li>
<li>5432 for the database</li>
</ul>
<p>You should check that they're running correctly first before moving on.</p>
<p>Press <code>Ctrl + C</code> or run <code>docker-compose down</code> to stop the containers.</p>
<h2 id="additional-setup">Additional setup</h2>
<p>We need to change a few settings,</p>
<p>Navigate to the <code>backend</code> folder and change <code>settings.py</code> to:</p>
<div class="highlight"><pre class="highlight python"><code><span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s">'ENGINE'</span><span class="p">:</span> <span class="s">'django.db.backends.postgresql_psycopg2'</span><span class="p">,</span>
<span class="s">'NAME'</span><span class="p">:</span> <span class="s">'postgres'</span><span class="p">,</span>
<span class="s">'USER'</span><span class="p">:</span> <span class="s">'postgres'</span><span class="p">,</span>
<span class="s">'PASSWORD'</span><span class="p">:</span> <span class="s">'postgres'</span><span class="p">,</span>
<span class="s">'HOST'</span><span class="p">:</span> <span class="s">'db'</span><span class="p">,</span>
<span class="s">'PORT'</span><span class="p">:</span> <span class="s">'5432'</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Also, we need to change CORS further below in <code>settings.py</code>. The CORS configuration allows our different applications to communicate across different domains in the web browser:</p>
<div class="highlight"><pre class="highlight python"><code><span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">[</span>
<span class="s">'corsheaders'</span><span class="p">,</span> <span class="c1"># add this
</span><span class="p">]</span>
<span class="n">MIDDLEWARE</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">...,</span>
<span class="s">"corsheaders.middleware.CorsMiddleware"</span><span class="p">,</span>
<span class="s">"django.middleware.common.CommonMiddleware"</span><span class="p">,</span>
<span class="p">...,</span>
<span class="p">]</span>
<span class="n">CORS_ALLOW_ALL_ORIGINS</span> <span class="o">=</span> <span class="bp">True</span>
</code></pre></div>
<h3 id="testing-the-stack-works-together">Testing the stack works together</h3>
<p>Finally, we'll build a simple response code to send data from the Django backend and ensure everything works together.</p>
<p>In <code>backend/backend</code>, create a new file, <code>views.py</code>, and paste the following:</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">JsonResponse</span>
<span class="k">def</span> <span class="nf">get_text</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="n">text</span> <span class="o">=</span> <span class="s">"All operational from the Django backend!"</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'text'</span><span class="p">:</span> <span class="n">text</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">JsonResponse</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</code></pre></div>
<p>Then open <code>urls.py</code> and add these two lines:</p>
<div class="highlight"><pre class="highlight python"><code><span class="kn">from</span> <span class="nn">.views</span> <span class="kn">import</span> <span class="n">get_text</span>
</code></pre></div>
<p>And in URL patterns:</p>
<div class="highlight"><pre class="highlight python"><code><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="c1"># other code
</span> <span class="n">path</span><span class="p">(</span><span class="s">'test/'</span><span class="p">,</span> <span class="n">get_text</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div><div class="highlight"><pre class="highlight python"><code><span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span>
<span class="kn">from</span> <span class="nn">.views</span> <span class="kn">import</span> <span class="n">get_text</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="p">.</span><span class="n">site</span><span class="p">.</span><span class="n">urls</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'test/'</span><span class="p">,</span> <span class="n">get_text</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div>
<p>You can now see this text on <a href="http://localhost:8000/test/">http://localhost:8000/test/</a>.</p>
<p>To display it in our frontend, navigate to <code>frontend/src/index.jsx</code> and change it to the following:</p>
<div class="highlight"><pre class="highlight jsx"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">preact</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">preactLogo</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./assets/preact.svg</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">./style.css</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">App</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">text</span><span class="p">,</span> <span class="nx">setText</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
<span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">http://127.0.0.1:8000/test/</span><span class="dl">'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=></span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">())</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setText</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">text</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>An unbeatable tech stack<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="p">=</span><span class="s">"https://preactjs.com"</span> <span class="na">target</span><span class="p">=</span><span class="s">"_blank"</span><span class="p">></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="p">=</span><span class="si">{</span><span class="nx">preactLogo</span><span class="si">}</span> <span class="na">alt</span><span class="p">=</span><span class="s">"Preact logo"</span> <span class="na">height</span><span class="p">=</span><span class="s">"160"</span> <span class="na">width</span><span class="p">=</span><span class="s">"160"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Running Preact, Django, Postgres, and Docker<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span><span class="si">{</span><span class="nx">text</span><span class="si">}</span><span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">(<</span><span class="nc">App</span> <span class="p">/>,</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">app</span><span class="dl">'</span><span class="p">));</span>
</code></pre></div>
<p>Refresh and reload, and you should see a working Preact front page!</p>
<h2 id="conclusion">Conclusion</h2>
<p>By containerizing your technology stack with Docker, you've taken a significant step toward seamless integration and enhanced efficiency. While the initial setup might have required some effort, the benefits are clear.</p>
<p>Now that you've containerized your tech stack, including Django, Preact, and Postgres, you can explore, innovate, and navigate the ever-evolving tech landscape more effectively—a pivotal moment in your tech journey. Embrace the possibilities, and may your adventures be characterized by streamlined integration and boundless creativity!</p>
Deploying serverless applications with Laravel Vaporhttps://www.honeybadger.io/blog/laravel-vapor/2024-02-13T00:00:00+00:002024-03-19T01:26:39+00:00Samson OmojolaThis article delves into the user-friendly world of Laravel Vapor. Say farewell to intricate setups and server management headaches. With Laravel Vapor, you'll experience the ease of deploying websites without the fuss.<p>In recent years, the serverless paradigm has become known as a transformative approach to application development, allowing us to focus more on writing code and less on managing infrastructure. Laravel Vapor, a serverless deployment platform for Laravel applications, brings the power of the Laravel framework into the world of serverless architecture. By integrating Laravel with AWS Lambda and other AWS services, Vapor offers a simple way to build, deploy, and scale applications without the complexities of traditional server management. In this comprehensive guide, we will discuss serverless architecture and the features and benefits of Laravel Vapor, as well as walk through the process of deploying a serverless application using Vapor.</p>
<h2 id="understanding-serverless-architecture">Understanding serverless architecture</h2>
<p>To understand the importance of Laravel Vapor and its role in building serverless applications, it's crucial to first understand the fundamentals of serverless architecture. Serverless computing is a cloud computing model that abstracts away the complexities of infrastructure management, allowing us to focus solely on writing code to implement business logic. In this model, we no longer need to provision, scale, or manage servers; instead, the cloud provider handles these tasks automatically.</p>
<p>At the heart of serverless architecture are functions. Functions, also known as serverless functions or AWS Lambda functions in the context of Laravel Vapor, are units of code that can be executed in response to specific events. These events could be HTTP requests, database changes, file uploads, or other triggers. Functions are stateless and ephemeral, meaning they only exist for the duration of their execution and do not maintain any persistent state between invocations.</p>
<p>Laravel Vapor harnesses the power of AWS Lambda, Amazon's serverless compute service, to execute Laravel applications in the form of serverless functions. This approach introduces a shift in how applications are built and deployed. Previously, we had to manage servers, infrastructure provisioning, and scaling to accommodate varying workloads. With Vapor, these concerns are abstracted, allowing us to focus on writing code that directly solves problems.</p>
<p>One of the key advantages of serverless architecture, and by extension, Laravel Vapor, is its ability to scale seamlessly. Traditional server setups often involve over-provisioning to handle occasional spikes in traffic. This results in wasted resources during periods of low demand. Serverless architecture, on the other hand, automatically scales up and down based on the incoming traffic, ensuring optimal use of resources and cost efficiency.</p>
<h3 id="exploring-the-benefits-of-laravel-vapor">Exploring the benefits of Laravel Vapor</h3>
<ol>
<li><p><strong>Seamless Integration with Laravel:</strong> One of the most compelling aspects of Laravel Vapor is its seamless integration with the Laravel framework. Vapor allows us to continue using the familiar Laravel tools, syntax, and paradigms we are accustomed to while reaping the benefits of serverless architecture. This integration eliminates the need for a steep learning curve, enabling us to transition smoothly into serverless development.</p></li>
<li><p><strong>Simplified Deployment:</strong> Deploying traditional applications often involves configuring servers, managing databases, and handling various server-related tasks. Laravel Vapor simplifies this process by abstracting away the underlying infrastructure management. We can focus solely on our application code and use Vapor's command-line tools to deploy our Laravel applications as serverless functions on AWS Lambda. This simplification accelerates the deployment pipeline and reduces the likelihood of deployment-related errors.</p></li>
<li><p><strong>Auto-Scaling and High Availability:</strong> Vapor leverages AWS Lambda's auto-scaling capabilities to ensure that your application can handle different levels of traffic. When the number of incoming requests increases, Lambda automatically provisions and manages the required resources to meet the demand. This high level of scalability guarantees that your application remains responsive, regardless of change in traffic.</p></li>
<li><p><strong>Cost Efficiency:</strong> Traditional hosting models often involve paying for resources that you may not use during periods of low traffic. Laravel Vapor's serverless architecture only uses resources when a function is actively processing requests. This "pay-as-you-go" model results in cost savings, as you are charged based on what your application uses rather than a fixed server capacity.</p></li>
<li><p><strong>Database and Storage Integration:</strong> Vapor seamlessly integrates with AWS services, such as Amazon RDS for databases and Amazon S3 for storage. This integration allows you to manage an application's data and assets while taking advantage of the reliability and scalability of AWS services.</p></li>
<li><p><strong>Custom Domains and SSL Certificates:</strong> Vapor makes it easy to associate custom domains with your serverless Laravel applications. You can also manage SSL certificates directly within the Vapor dashboard, ensuring secure communication between clients and your serverless functions.</p></li>
<li><p><strong>Environment Management:</strong> Vapor provides a robust environment management system that allows you to define different environments (e.g., development, staging, and production) and configure them separately. This facilitates testing, debugging, and deploying changes with confidence.</p></li>
<li><p><strong>Background Jobs and Queues:</strong> Laravel's queue system seamlessly integrates with Vapor, allowing you to handle background jobs and tasks asynchronously. Vapor supports queue workers powered by AWS Lambda, enabling efficient processing of tasks without the need for dedicated infrastructure.</p></li>
</ol>
<h2 id="getting-started-setting-up-laravel-vapor">Getting started: setting up Laravel Vapor</h2>
<p>Now that we've explored the foundational concepts of serverless architecture and delved into the features of Laravel Vapor, it's time to roll up our sleeves and walk through the process of setting up Laravel Vapor and preparing our development environment for building serverless applications.</p>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>A basic understanding of Laravel framework concepts.</li>
<li>An AWS account with appropriate permissions to create and manage resources.</li>
<li>Composer installed on your local machine for Laravel application management.</li>
</ul>
<h3 id="installing-vapor">Installing Vapor</h3>
<p>To begin, install the Laravel Vapor CLI tool globally using Composer:</p>
<div class="highlight"><pre class="highlight shell"><code>composer global require laravel/vapor-cli
</code></pre></div>
<p>Make sure that Composer's global bin directory is added to your system's PATH so that you can access the Vapor CLI tool from anywhere in your terminal.</p>
<h3 id="configure-aws-credentials">Configure AWS credentials</h3>
<p>Since Laravel Vapor relies on Amazon Web Services (AWS) infrastructure, you'll need to configure your AWS credentials to enable communication between your application and AWS services. Follow these steps to set up your AWS credentials:</p>
<ol>
<li>If you don't have an AWS account, sign up for one at <a href="https://aws.amazon.com/">AWS Sign-Up</a>.</li>
<li>Once you have an AWS account, navigate to the <a href="https://console.aws.amazon.com/iam/">IAM (Identity and Access Management)</a> section of the AWS Management Console.</li>
<li>Create a new IAM user with programmatic access. This user will be utilized to interact with AWS services from the Vapor CLI. Attach the "AdministratorAccess" policy to this user for now. Note the Access Key ID and Secret Access Key generated for this user.</li>
<li>Configure the AWS CLI on your local machine by running the following command:</li>
</ol>
<div class="highlight"><pre class="highlight shell"><code>aws configure
</code></pre></div>
<p>Provide the Access Key ID, Secret Access Key, default region, and preferred output format when prompted. These credentials will be stored in the <code>~/.aws/credentials</code> file.</p>
<p>Your AWS credentials are now set up, allowing Laravel Vapor to communicate securely with AWS services.</p>
<h3 id="verify-vapor-cli-installation">Verify Vapor CLI installation</h3>
<p>To ensure that the Vapor CLI is properly installed and configured, run the following command:</p>
<div class="highlight"><pre class="highlight shell"><code>vapor
</code></pre></div>
<p>You should see the Vapor CLI's help information, indicating that the installation was successful. With the Vapor CLI installed and your AWS credentials configured, you're ready to move on to creating a Laravel application that will be transformed into a serverless application using Laravel Vapor.</p>
<h3 id="creating-a-laravel-application">Creating a Laravel application</h3>
<p>If you already have a Laravel application, you can skip this step and proceed to the next section.</p>
<p>Open your terminal and navigate to the directory where you want to create a new Laravel application. Run the following command to generate a new Laravel project:</p>
<div class="highlight"><pre class="highlight shell"><code>composer create-project <span class="nt">--prefer-dist</span> laravel/laravel serverless-app
</code></pre></div>
<p>This command will create a new Laravel application in a directory named <code>serverless-app</code>.</p>
<h3 id="set-up-basic-routes-and-controllers">Set up basic routes and controllers</h3>
<p>Next, let's set up some basic routes and controllers for our application. Open the routes/web.php file and define a simple route:</p>
<div class="highlight"><pre class="highlight php"><code><span class="nc">Route</span><span class="o">::</span><span class="nf">get</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span> <span class="s1">'HomeController@index'</span><span class="p">);</span>
</code></pre></div>
<p>Now, create a new controller named <code>HomeController</code> by running the following command:</p>
<div class="highlight"><pre class="highlight php"><code><span class="n">php</span> <span class="n">artisan</span> <span class="n">make</span><span class="o">:</span><span class="n">controller</span> <span class="nc">HomeController</span>
</code></pre></div>
<p>Open the generated HomeController.php file located in the app/Http/Controllers directory and define a basic method:</p>
<div class="highlight"><pre class="highlight php"><code><span class="k">public</span> <span class="k">function</span> <span class="n">index</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">return</span> <span class="nf">view</span><span class="p">(</span><span class="s1">'welcome'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>This method will return the <code>welcome</code> view, which is a basic HTML template provided by Laravel.</p>
<h3 id="verify-the-application">Verify the application</h3>
<p>To verify that your application is running correctly, start the development server by running the following:</p>
<div class="highlight"><pre class="highlight shell"><code>php artisan serve
</code></pre></div>
<p>Open your web browser and visit <code>http://localhost:8000</code>. You should see the default Laravel welcome page.</p>
<p>With our basic Laravel application set up, we're now ready to use Laravel Vapor with it.</p>
<h3 id="configuring-vapor-for-your-application">Configuring Vapor for your application</h3>
<p>This step includes setting up the necessary environment variables, configuring deployment settings, and preparing your application for deployment</p>
<p>Install Laravel Vapor:</p>
<div class="highlight"><pre class="highlight shell"><code>composer require laravel/vapor
</code></pre></div>
<p>This command will add the Vapor package to your project's dependencies.</p>
<h3 id="configure-environment-variables">Configure environment variables</h3>
<p>Laravel Vapor uses environment variables to manage configuration settings specific to your deployment environments. Create a .env.vapor file in your project root to define these variables. Here's an example of a basic configuration:</p>
<div class="highlight"><pre class="highlight php"><code><span class="no">APP_ENV</span><span class="o">=</span><span class="n">production</span>
<span class="no">APP_KEY</span><span class="o">=</span><span class="n">your</span><span class="o">-</span><span class="n">app</span><span class="o">-</span><span class="n">key</span>
<span class="no">AWS_DEFAULT_REGION</span><span class="o">=</span><span class="n">your</span><span class="o">-</span><span class="n">region</span>
<span class="no">AWS_BUCKET</span><span class="o">=</span><span class="n">your</span><span class="o">-</span><span class="n">bucket</span><span class="o">-</span><span class="n">name</span>
</code></pre></div>
<p>Replace your-app-key, your-region, and your-bucket-name with appropriate values. The APP_ENV should be set to production for Vapor deployments.</p>
<h3 id="configure-deployment-settings">Configure deployment settings</h3>
<p>In your vapor.yml file located in the project root directory, you can configure various settings related to deployment. This includes specifying the environment variables file, setting up domains, and more. Here's a minimal example:</p>
<div class="highlight"><pre class="highlight php"><code><span class="n">id</span><span class="o">:</span> <span class="n">your</span><span class="o">-</span><span class="n">vapor</span><span class="o">-</span><span class="n">app</span><span class="o">-</span><span class="n">id</span>
<span class="n">name</span><span class="o">:</span> <span class="nc">Your</span> <span class="nc">Vapor</span> <span class="nc">App</span> <span class="nc">Name</span>
<span class="n">environments</span><span class="o">:</span>
<span class="n">production</span><span class="o">:</span>
<span class="n">domain</span><span class="o">:</span> <span class="n">your</span><span class="o">-</span><span class="n">domain</span><span class="mf">.</span><span class="n">com</span>
</code></pre></div>
<p>Replace your-vapor-app-id with your actual Vapor app ID, Your Vapor App Name with your desired app name, and your-domain.com with your domain name.</p>
<h3 id="prepare-for-deployment">Prepare for deployment</h3>
<p>Before deploying your application, make sure you're using the vapor environment. Update your .env file to include the following:</p>
<div class="highlight"><pre class="highlight php"><code><span class="no">APP_ENV</span><span class="o">=</span><span class="n">vapor</span>
</code></pre></div>
<p>Additionally, you can set the VAPOR_DEBUG variable to true to enable debugging output during deployments:</p>
<div class="highlight"><pre class="highlight php"><code><span class="no">VAPOR_DEBUG</span><span class="o">=</span><span class="kc">true</span>
</code></pre></div>
<h3 id="generate-assets">Generate assets</h3>
<p>If your application includes assets like JavaScript, CSS, or images, generate the optimized assets using Laravel's mix tool:</p>
<div class="highlight"><pre class="highlight shell"><code>npm <span class="nb">install
</span>npm run production
</code></pre></div>
<h3 id="deploy-your-application">Deploy your application</h3>
<p>With everything configured, you're ready to deploy your application using Laravel Vapor. In your terminal, run the following command:</p>
<div class="highlight"><pre class="highlight shell"><code>vapor deploy production
</code></pre></div>
<p>This command will package your application, create AWS Lambda functions, set up the required infrastructure, and deploy your application to the specified environment.</p>
<h3 id="observe-the-deployment-process">Observe the deployment process</h3>
<p>When you execute the vapor deploy production command, Laravel Vapor performs several tasks behind the scenes:</p>
<ul>
<li>Packages your application code and assets into deployment archives.</li>
<li>Creates AWS Lambda functions for your application's routes.</li>
<li>Sets up API Gateway to manage HTTP requests.</li>
<li>Configures necessary environment variables and AWS services.</li>
<li>Deploys your application to multiple AWS regions for global distribution.</li>
</ul>
<p>Observe the command's output to get insights into the deployment process, including the creation of Lambda functions, API Gateway stages, and other AWS resources.</p>
<h3 id="access-your-deployed-application">Access your deployed application</h3>
<p>After successful deployment, Vapor provides a URL where your application is accessible. This URL will be based on the configured domain in your vapor.yml file. Visit the URL in your web browser to see your deployed Laravel application running in the serverless environment.</p>
<h3 id="testing-and-quality-assurance">Testing and quality assurance</h3>
<p>After deployment, thoroughly test your application's functionality to ensure that everything is working as expected in the serverless environment. Pay close attention to any potential differences between your local development environment and the serverless environment, such as storage and database interactions.</p>
<h3 id="monitoring-and-scaling">Monitoring and scaling</h3>
<p>Laravel Vapor provides built-in monitoring tools to help track the performance and usage of your application. Use the AWS CloudWatch dashboard to monitor key metrics and diagnose issues that may arise. Additionally, take advantage of Vapor's automatic scaling capabilities to handle varying levels of traffic without manual intervention.</p>
<h3 id="continuous-deployment">Continuous deployment</h3>
<p>As updates are made to your application, you can leverage continuous deployment practices to streamline the deployment process. Whenever you're ready to deploy a new version of an application, simply run the vapor deploy production command again. Vapor will automatically update the necessary AWS resources and distribute the new version to your global deployment.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Congratulations! You've successfully journeyed through the process of building and deploying a serverless Laravel application using Laravel Vapor. You've gained insights into serverless architecture and learned how Vapor simplifies deployment. Thanks for reading!</p>
Visualizing Ahoy analytics in Railshttps://www.honeybadger.io/blog/ahoy-rails-analytics/2024-02-12T00:00:00+00:002024-03-19T01:26:39+00:00Joshua WoodHoneybadger co-founder Joshua Wood explains how to graph Ahoy page views in Rails with Chartkick, with a preview of our upcoming observability tool—Insights!<iframe width="100%" height="100%" style="aspect-ratio: 560 / 315; margin-bottom: 1rem;" src="https://www.youtube-nocookie.com/embed/o4qo3ulxps8?si=hNu00Usn6yTRWzXQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
<p>At Honeybadger, we use <a href="https://github.com/ankane/ahoy">Ahoy</a> for first-party analytics in Rails. Ahoy is excellent for developers because it lives in your Rails application alongside your other data and code. Want to answer a specific question about your product or website visitors? It's just one <code>ActiveRecord</code> query away:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="no">Ahoy</span><span class="o">::</span><span class="no">Event</span><span class="p">.</span><span class="nf">where_event</span><span class="p">(</span><span class="s2">"$view"</span><span class="p">,</span> <span class="ss">page: </span><span class="s2">"/"</span><span class="p">).</span><span class="nf">count</span>
</code></pre></div>
<p>Ahoy also works with <a href="https://www.chartkick.com/">Chartkick</a> and <a href="https://github.com/ankane/groupdate">Groupdate</a> to visualize your data. For example, after installing the two libraries, you can add a chart of all page views to any Rails view:</p>
<div class="highlight"><pre class="highlight erb"><code><span class="cp"><%=</span>
<span class="n">line_chart</span> <span class="no">Ahoy</span><span class="o">::</span><span class="no">Event</span><span class="p">.</span>
<span class="nf">where_event</span><span class="p">(</span><span class="s2">"$view"</span><span class="p">,</span> <span class="ss">page: </span><span class="s2">"/"</span><span class="p">).</span>
<span class="nf">group_by_hour</span><span class="p">(</span><span class="ss">:time</span><span class="p">).</span>
<span class="nf">count</span>
<span class="cp">%></span>
</code></pre></div>
<p>The above code produces a chart like this:</p>
<p><img src="/images/blog/posts/ahoy-rails-analytics/ahoy-chart.png" alt="A line graph with a time series on the x-axis ranging from "Feb 7, 2 PM" to "Feb 9, 8 AM". The y-axis starts at 0 and extends upward, though the upper limit isn't visible; it's at least 600 based on the graph's peak. The line plot has significant fluctuations: starting at a value around 100, it spikes to just above 600, drops back near 100, rises again slightly above 200, then drops to nearly 0 where it remains flat until a small uptick near "Feb 9, 8 AM". This could represent a metric like website visitors or system performance over time, indicating a peak in activity or usage early on, followed by a period of inactivity or normal levels. The context of the data is not provided in the image." />
<em>Charting Ahoy data with Chartkick</em></p>
<p>Ahoy and Chartkick are great for tracking analytics and building dashboards, but the downside is that you must deploy your app to make changes. If you find yourself regularly deploying to update your dashboards, you could try using <a href="https://github.com/ankane/blazer">Blazer</a>, which lets you explore your data and create charts and dashboards with SQL.</p>
<p><img src="/images/blog/posts/ahoy-rails-analytics/blazer.png" alt="A screenshot of a web-based database interface showing a SQL query result. The SQL command "SELECT * FROM "ratings" LIMIT 10" is visible, suggesting the user has queried the first 10 rows from a 'ratings' table. Displayed are columns for 'id', 'user_id', 'movie_id', 'rating', and 'rated_at'. Five rows are visible." />
<em>Screenshot from Blazer README</em></p>
<p>That said, SQL can be complex to work with. At Honeybadger, we've used the Ahoy+Chartkick+Blazer stack for years and recently augmented Ahoy's data store to send events to <a href="/tour/logging-observability">Honeybadger Insights</a> so that we can explore our web analytics and create dashboards alongside our other observability data.</p>
<h2 id="integrating-ahoy-with-honeybadger-insights">Integrating Ahoy with Honeybadger Insights</h2>
<p>Honeybadger Insights is a new <a href="/tour/logging-observability">logging and observability tool</a> that we recently added to Honeybadger. You can use our query language—<em>BadgerQL</em>—to dive into your Honeybadger data (errors, uptime events, etc.) and create charts and dashboards from BadgerQL queries. You can also ship us your application logs and custom events to have all of your data in one place.</p>
<p>Here's how to integrate Ahoy with Honeybadger Insights and create a similar chart to Chartkick using BadgerQL.</p>
<p>In Rails, if you aren't using the <code>honeybadger</code> gem yet, you'll need to install it. Follow the <a href="https://docs.honeybadger.io/lib/ruby/integration-guides/rails-exception-tracking/#installation">Rails integration instructions</a> (usually takes just a few minutes).</p>
<p>If you're already use the <code>honeybadger</code> gem, I recommend upgrading to the latest version. First, make sure the <code>honeybadger</code> gem is pinned to version <code>~> 5.5</code> in your <code>Gemfile</code>:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># Gemfile</span>
<span class="n">gem</span> <span class="s2">"honeybadger"</span><span class="p">,</span> <span class="s2">"~> 5.5"</span>
</code></pre></div>
<p>Then upgrade to the latest version:</p>
<div class="highlight"><pre class="highlight shell"><code>bundle update honeybadger
</code></pre></div>
<p>Next, open <code>config/initializers/ahoy.rb</code> in a text editor, and modify the default <code>Ahoy::Store</code> to look like this:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Ahoy::Store</span> <span class="o"><</span> <span class="no">Ahoy</span><span class="o">::</span><span class="no">DatabaseStore</span>
<span class="k">def</span> <span class="nf">track_visit</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="no">Honeybadger</span><span class="p">.</span><span class="nf">event</span><span class="p">(</span><span class="s2">"ahoy_visit"</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
<span class="k">super</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">track_event</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="no">Honeybadger</span><span class="p">.</span><span class="nf">event</span><span class="p">(</span><span class="s2">"ahoy_event"</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
<span class="k">super</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">geocode</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="no">Honeybadger</span><span class="p">.</span><span class="nf">event</span><span class="p">(</span><span class="s2">"ahoy_geocode"</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
<span class="k">super</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">authenticate</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="no">Honeybadger</span><span class="p">.</span><span class="nf">event</span><span class="p">(</span><span class="s2">"ahoy_authenticate"</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
<span class="k">super</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>That's it. To test in your development environment, you can run <code>rails server</code> with the <code>HONEYBADGER_REPORT_DATA=true</code> environment variable to temporarily turn off Honeybadger's development mode.</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nv">HONEYBADGER_REPORT_DATA</span><span class="o">=</span><span class="nb">true </span>rails server
</code></pre></div>
<p>As Ahoy collects events, you should see them on the <em>Insights</em> tab in your Honeybadger project:</p>
<p><img src="/images/blog/posts/ahoy-rails-analytics/insights-ahoy-events.png" alt="A screenshot of an insights dashboard in Honeybadger. It shows a search interface with a query for fields 'ts' and 'preview' sorted by 'ts' (timestamp). The timeframe of "2d -> now" indicates a search for the past two days up to the current time in PST (Pacific Standard Time). Below the search bar, the results display an "Events" view selected from a dropdown that also includes a "Line" view. Each entry corresponds to a timestamped event, labeled with "@ts" for the time in PST and "@preview" which contains JSON formatted data. The JSON data includes an "event_type": "ahoy_event" and an "event_id", with each event having a unique ID. All events are tagged with the name "$view", suggesting they are page or view-related events within the application, possibly tracking user interactions. The events are listed in reverse chronological order, the most recent being at "2024-02-09 12:02:11.000" and the oldest visible at "2024-02-09 11:53:53.000". The green checkmark indicates 100 of 100 results displayed successfully." />
<em>Ahoy events in Honeybadger Insights</em></p>
<p>To create a chart similar to the Chartkick example above, drop the following BadgerQL into the query box and hit <em>Search</em>.</p>
<div class="highlight"><pre class="highlight plaintext"><code>filter event_type::str == "ahoy_event"
| filter name::str == "$view"
| stats count() by bin(1h) as date
| fill date step 1h
| sort date
</code></pre></div>
<p><img src="/images/blog/posts/ahoy-rails-analytics/insights-ahoy-query.png" alt="A web analytics dashboard with a search query for event tracking, specifically filtering for "ahoy_event" of type "$view". The results are aggregated by the count of events per hour, sorted by date and time in PST. The table shows several rows with 'count()' and corresponding 'date' columns, with counts of 549, 296, 76, and 270 events at different hourly intervals on February 7, 2024, indicating fluctuating user engagement over time. The latest times show zero events, suggesting no activity or data collection during those periods." />
<em>For a walkthrough of the BadgerQL in this query, <a href="https://youtu.be/o4qo3ulxps8">check out my screencast on YouTube</a></em></p>
<p>Finally, toggle the <em>Line</em> button to visualize the data as a line chart:</p>
<p><img src="/images/blog/posts/ahoy-rails-analytics/insights-ahoy-chart.png" alt="A line chart on a web analytics dashboard displays event counts over time. A prominent spike to nearly 600 events occurs around 17:00 on February 7, with other notable activity around 06:00 on February 9. The x-axis covers a period from 18:00 on one day to 12:00 the next, while the y-axis ranges from 0 to 600. The chart is part of the Honeybadger app interface with query filters visible, set to the PST timezone." />
<em>Ahoy chart in Honeybadger Insights</em></p>
<p>Sending our Ahoy data to Insights means we can query our web/product analytics and user events in the same place as our observability data. It's a significant upgrade for product-led teams like ours. If that sounds like you, check it out!</p>
Building reusable UI components in Rails with ViewComponenthttps://www.honeybadger.io/blog/rails-viewcomponent/2024-02-07T00:00:00+00:002024-03-19T01:26:39+00:00Michael BarasaHarness the power of modularity in Rails. Learn how to build reusable UI components to reduce code duplication and scale your design system.<p>Reusable UI components are user interface widgets or elements that can be used in various places in a web project. These components are typically small and created for a specific functionality.</p>
<p>Typically, you write the code once and then import the UI components wherever needed. For example, you can create a card component that displays certain information or have a navigation bar that will appear at the top of all web pages. Such UI elements can be imported and added to your desired web page. Other examples of reusable UI components are input fields, buttons, modals, and progress bars.</p>
<p>There are multiple reasons as to why you should use reusable UI components:</p>
<ul>
<li>They help you save time and effort. As the name suggests, these UI elements can be reused on different pages rather than coding everything from scratch.</li>
<li>Reusable UI components improve code quality and maintainability and bring more consistency to your project.</li>
<li>Having reusable components also makes your project more modular, allowing you to break down the task into smaller, more manageable chunks.</li>
<li>Tracking down and isolating errors and bugs in a modular application is always easier.</li>
</ul>
<p>In this tutorial, we will help you understand how to identify reusable components in your project and how to develop these UI widgets in Rails. Let's jump in.</p>
<h2 id="identifying-and-extracting-reusable-components">Identifying and extracting reusable components</h2>
<p>It's always a good practice to analyze the structure of your application and identify parts that can be broken down into smaller, reusable components. To do this, start by looking at common functional patterns evident at multiple places in your application. Be sure to pay attention to the relationship between models, controllers, and views to identify areas where they share logic or functionality.</p>
<p>You should also inspect your application, looking for instances of repetition in the code. When you find duplicate code in your application, it may indicate that a reusable component is required. Reviewing your view templates and files can help you find common HTML elements, forms, and other components to modularize.</p>
<h2 id="creating-reusable-components">Creating reusable components</h2>
<p>In this step, we will learn how to create a reusable component in Rails using <a href="https://viewcomponent.org/">ViewComponent</a>.</p>
<h3 id="using-viewcomponent">Using ViewComponent</h3>
<p>ViewComponent is a Ruby on Rails library that helps create reusable and encapsulated UI components for your application. By breaking down an application into smaller chunks, this framework makes it easy to perform unit tests and track down sources of error, facilitating faster debugging.</p>
<p>We will create the following simple website using reusable components. Specifically, we will create two reusable UI components: the navigation bar and the product card.</p>
<p><img src="/images/blog/posts/rails-viewcomponent/website.png" alt="Simple website" /></p>
<h3 id="getting-started">Getting started</h3>
<p>Add the following code to your Gemfile.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">gem</span> <span class="s2">"view_component"</span>
</code></pre></div>
<p>Run the <code>bundle install</code> command to install the ViewComponent library into your project. You should see the following output if the installation was successful.</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nv">$ </span>bundle <span class="nb">install
</span>Fetching gem metadata from https://rubygems.org/
Resolving dependencies...
Using rake 13.0.6
Using concurrent-ruby 1.2.2
Fetching view_component 3.6.0
Using rails 7.0.8
Installing view_component 3.6.0
Bundle <span class="nb">complete</span><span class="o">!</span> 15 Gemfile dependencies, 76 gems now installed.
Use <span class="sb">`</span>bundle info <span class="o">[</span>gemname]<span class="sb">`</span> to see where a bundled gem is installed.
</code></pre></div>
<h3 id="creating-components">Creating components</h3>
<p>Components are subclasses of <code>ViewComponent::Base</code>. Whenever we create a ViewComponent, an <code>app/components</code> directory is also generated to store all of our components' files.</p>
<p>When naming components, it's best practice to name them according to what they do or render instead of what they accept. For example, we can have a <code>CardComponent</code> rather than a <code>ReportComponent</code>.</p>
<p>Components can contain HTML, CSS, and JavaScript code. We can also add logic to guide the behavior of these components in a separate Ruby class.</p>
<p>We can create a ViewComponent by running the following code. In this case, we'll create a simple navigation component using the generator. We can pass a name or <a href="https://viewcomponent.org/guide/generators.html">other arguments</a> to the component.</p>
<div class="highlight"><pre class="highlight shell"><code>bin/rails generate component Navigation nav
</code></pre></div>
<p>You should see the following outputs in the terminal.</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nv">$ </span>bin/rails generate component Navigation nav
create app/components/navigation_component.rb
invoke test_unit
create <span class="nb">test</span>/components/navigation_component_test.rb
invoke erb
create app/components/navigation_component.html.erb
</code></pre></div>
<p>As shown above, we have generated a <code>navigation_component</code> file to store our component's logic and the <code>navigation_componnet.html.erb</code> for our actual HTML and CSS code.</p>
<p>The <code>navigation_component</code> should appear as follows:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># frozen_string_literal: true</span>
<span class="k">class</span> <span class="nc">NavigationComponent</span> <span class="o"><</span> <span class="no">ViewComponent</span><span class="o">::</span><span class="no">Base</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">nav</span><span class="p">:)</span>
<span class="vi">@nav</span> <span class="o">=</span> <span class="n">nav</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>While the <code>navigation_component.html.erb</code> file looks as shown below:</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div></span>Add Navigation template here<span class="nt"></div></span>
</code></pre></div>
<p>As shown below, we can add some <code>nav</code> elements to the template. We're using TailwindCSS for styling, but you can also use custom stylesheets.</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"bg-red-200 py-4 border-b text-xl"</span><span class="nt">></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"flex"</span><span class="nt">></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"mr-6"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">class=</span><span class="s">"hover:text-blue-800"</span> <span class="na">href=</span><span class="s">"#"</span><span class="nt">></span>Home<span class="nt"></a></span>
<span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"mr-6"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">class=</span><span class="s">"hover:text-blue-800"</span> <span class="na">href=</span><span class="s">"#"</span><span class="nt">></span>Phones<span class="nt"></a></span>
<span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"mr-6"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">class=</span><span class="s">"hover:text-blue-800"</span> <span class="na">href=</span><span class="s">"#"</span><span class="nt">></span>TVs<span class="nt"></a></span>
<span class="nt"></li></span>
<span class="nt"></ul></span>
<span class="nt"></div></span>
</code></pre></div>
<p>We can render the component by adding the following code snippet in the respective view. In this case, we will render the element in the `application.html.erb' layout file since we want it displayed across all web pages.</p>
<p>We use the <code><%= render NavigationComponent.new(nav: @nav) %></code> code to render the nav component in the browser, as shown below.</p>
<div class="highlight"><pre class="highlight erb"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><title></span>Shopit<span class="nt"></title></span>
<span class="nt"><meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width,initial-scale=1"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">csrf_meta_tags</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">csp_meta_tag</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">stylesheet_link_tag</span> <span class="s2">"tailwind"</span> <span class="p">,</span> <span class="s2">"inter-font"</span><span class="p">,</span> <span class="s2">"data-turbo-track"</span><span class="p">:</span> <span class="s2">"reload"</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">stylesheet_link_tag</span> <span class="s2">"application"</span><span class="p">,</span> <span class="s2">"data-turbo-track"</span><span class="p">:</span> <span class="s2">"reload"</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">javascript_importmap_tags</span> <span class="cp">%></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><main</span> <span class="na">class=</span><span class="s">"container px-6"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="no">NavBarComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">()</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="k">yield</span> <span class="cp">%></span>
<span class="nt"></main></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div>
<p>Let's go ahead and create a new CardComponent that we will use to display items retrieved from the database to the user.</p>
<p>To generate the component, we run the following command.</p>
<div class="highlight"><pre class="highlight shell"><code>bin/rails generate component ItemCardComponent title description
</code></pre></div>
<p>In this case, the <code>title</code> and <code>description</code> are the product variables we wish to display to users. Once again, you should see the <code>item_card_component.rb</code> and <code>item_card_component.html.erb</code> files in the <code>components</code> folder.</p>
<p>Here's how the <code>item_card_component.rb</code> should appear. We will later pass the title and description parameters in the <code>index.html.erb</code> file:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># frozen_string_literal: true</span>
<span class="k">class</span> <span class="nc">ItemCardComponent</span> <span class="o"><</span> <span class="no">ViewComponent</span><span class="o">::</span><span class="no">Base</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">title</span><span class="p">:,</span> <span class="n">description</span><span class="p">:)</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="n">title</span>
<span class="vi">@description</span> <span class="o">=</span> <span class="n">description</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>We can retrieve the values passed to the ItemCardComponent in <code>item_card_component.html.erb</code>, with the following.</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"max-w-sm rounded overflow-hidden shadow-lg m-2 p-4 bg-green-400"</span><span class="nt">></span>
<span class="nt"><h4</span> <span class="na">class=</span><span class="s">"font-bold text-xl mb-2"</span><span class="nt">></span><span class="cp"><%=</span> <span class="vi">@title</span> <span class="cp">%></span><span class="nt"></h4></span>
<span class="nt"><p</span> <span class="na">class=</span><span class="s">"text-gray-400 text-base"</span><span class="nt">></span><span class="cp"><%=</span> <span class="vi">@description</span> <span class="cp">%></span><span class="nt"></p></span>
<span class="nt"></div></span>
</code></pre></div>
<p>We can also create a BannerComponent, which allows us to communicate certain information to users using the following command.</p>
<div class="highlight"><pre class="highlight shell"><code>bin/rails generate component banner title message
</code></pre></div>
<p>The BannerComponent will accept a title and message when initialized.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># frozen_string_literal: true</span>
<span class="k">class</span> <span class="nc">BannerComponent</span> <span class="o"><</span> <span class="no">ViewComponent</span><span class="o">::</span><span class="no">Base</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">title</span><span class="p">:,</span> <span class="n">message</span><span class="p">:)</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="n">title</span>
<span class="vi">@message</span> <span class="o">=</span> <span class="n">message</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>We will then access these values in the <code>banner_component.html.erb</code> file and display them to the user.</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"bg-blue-500 text-white p-4"</span><span class="nt">></span>
<span class="nt"><h1</span> <span class="na">class=</span><span class="s">"text-2xl font-bold mb-2"</span><span class="nt">></span><span class="cp"><%=</span> <span class="vi">@title</span> <span class="cp">%></span><span class="nt"></h1></span>
<span class="nt"><p</span> <span class="na">class=</span><span class="s">"text-lg"</span><span class="nt">></span><span class="cp"><%=</span> <span class="vi">@message</span> <span class="cp">%></span><span class="nt"></p></span>
<span class="nt"></div></span>
</code></pre></div>
<p>Finally, we can render the ItemCardComponent and BannerComponent in the <code>index.html.erb</code> file using the <code>render()</code> method, as shown below.</p>
<div class="highlight"><pre class="highlight erb"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"flex-auto"</span><span class="nt">></span>
<span class="nt"><h1</span> <span class="na">class=</span><span class="s">"text-4xl mb-4"</span><span class="nt">></span>Dashboard<span class="nt"></h1></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"grid grid-cols-3 gap-4"</span><span class="nt">></span>
<span class="cp"><%</span> <span class="vi">@products</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">product</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">render</span><span class="p">(</span><span class="no">ItemCardComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="n">product</span><span class="p">.</span><span class="nf">title</span><span class="p">,</span> <span class="ss">description: </span><span class="n">product</span><span class="p">.</span><span class="nf">description</span><span class="p">))</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="cp"><%=</span> <span class="n">render</span><span class="p">(</span><span class="no">BannerComponent</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Status"</span><span class="p">,</span> <span class="ss">message: </span><span class="s2">"Completed"</span><span class="p">))</span> <span class="cp">%></span>
<span class="nt"></div></span>
</code></pre></div>
<p>You can download the full code used in this tutorial from this <a href="https://github.com/WanjaMIKE/reusablecomponentsinrail">GitHub repository</a>.</p>
<h2 id="best-practices-for-building-reusable-ui-components">Best practices for building reusable UI components</h2>
<p>Reusable components are an essential part of front-end web development. They allow you to break down large complex UIs into smaller components, which are easier to manage and debug. Here are some principles you should follow when creating reusable UI components.</p>
<ul>
<li>Ensure that each component plays a specific responsibility to make it easier to understand their usage and how the components fit into a project.</li>
<li>Use props to pass values to UI. Reusable components are flexible, configurable, and dynamic. They can accept different types of data, including collections, strings, and objects, which extends their usability.</li>
<li>Consider separation of concerns. A ruby file is also created whenever you generate a component with the ViewComponent library. You can add your logic to this <code>.rb</code> file and access it directly in your view component. Separation of concerns keeps your code clean and allows you to identify errors in your project quickly.</li>
<li>Add component-specific styling. Instead of combining your CSS styles in one file, you can include CSS inside specific component files to achieve that minimalistic look.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>In this article, we've learned how to create reusable components in rails using ViewComponent. These UI components can help achieve consistency in design and functionality across your application. You can also combine these UI elements to create complex UI designs. Consider implementing reusable components using ViewComponent in your next Ruby on Rails project.</p>
Password hashing in Node.js with bcrypthttps://www.honeybadger.io/blog/node-password-hashing/2024-01-30T00:00:00+00:002024-03-19T01:26:39+00:00Samson OmojolaBoosting your Node.js app's safety: a guide to keeping user passwords secure with bcrypt. Learn how to safeguard user accounts and data in your applications for better peace of mind.<p>In the digital age, security is of paramount importance, especially when it comes to user data and credentials. As developers, we must ensure that sensitive information, such as passwords, is stored securely to prevent unauthorized access and potential breaches. One of the key techniques employed to enhance security is password hashing, and in the realm of Node.js development, the bcrypt library stands out as a robust solution for this purpose.</p>
<p>In this comprehensive article, we will delve into the world of password hashing and explore how to implement it effectively using the bcrypt library in a Node.js application. We'll cover the fundamental concepts of password security, the drawbacks of storing plain passwords, and how bcrypt addresses these challenges. By the end of this article, you'll have a solid understanding of the importance of password hashing and be equipped with practical knowledge to integrate bcrypt into your Node.js projects for safeguarding user credentials.</p>
<h2 id="why-password-security-matters">Why password security matters</h2>
<p>Passwords have been the go-to method for proving your identity when using websites and apps for a while. However, there's a big problem with them; if they're not protected well, like being kept in plain text or with weak encryption, an attacker who gets into the system can easily gain access to your account and private information. This is a danger for both the average user and businesses.</p>
<p>To mitigate this risk, password security protocols aim to change plain text passwords into an unintelligible format that cannot be easily reversed. This process, known as password hashing, involves applying cryptographic algorithms to the original password and generating a hash value that appears random and unrelated to the original input. Even if an attacker gains access to the hashed passwords, it should be quite difficult for them to convert it to the original password.</p>
<h2 id="the-pitfalls-of-storing-plain-passwords">The pitfalls of storing plain passwords</h2>
<p>Storing passwords in plain text within databases might seem like an easy way to keep track of them, but it's a massive security problem. When a database gets hacked, attackers can instantly see everyone's login details. This is especially bad news for people who use the same passwords for lots of different websites because all their accounts are now vulnerable. However, even if you consistently use unique passwords, there's still a risk because of something called "rainbow table attacks". This is when attackers use pre-made lists of hash values to quickly match them up with stolen hashes.</p>
<p>To deal with these problems, we use something called "password hashing". It's like supercharging the security of an app. One popular tool for this is called "bcrypt", which is proven to be highly effective at protecting passwords. It makes it difficult for attackers to guess passwords by trying lots of combinations and thwarts rainbow table attacks.</p>
<h2 id="introducing-bcrypt">Introducing bcrypt</h2>
<p>Bcrypt is a widely used library for securely hashing passwords. It relies on the Blowfish cipher and a technique called "salting" to hash passwords. Salting involves adding a random value (called the "salt") to the original password before hashing it. This ensures that even if two users happen to have the same password, their hashed values will be different because of the unique salt added to each one.</p>
<p>By using a cryptographically secure hash function, bcrypt significantly slows down the hashing process, making it computationally expensive. This is a good thing because it makes it much harder for attackers to use brute-force methods to guess passwords.</p>
<p>In the upcoming sections of this article, we'll take a deep dive into how bcrypt works. We'll look at how it can be used within Node.js applications and break down the steps needed to hash and check passwords securely. We'll provide practical examples to help you understand how to use bcrypt effectively to enhance the security of your application.</p>
<h2 id="understanding-bcrypts-workings">Understanding bcrypt's workings</h2>
<p>To fully appreciate the power of bcrypt, it's important to delve into the underlying mechanisms that make it an excellent choice for password hashing in Node.js applications. Let's break down the key components and processes involved in bcrypt's operation:</p>
<ul>
<li>Salt generation</li>
</ul>
<p>The first step in bcrypt's password hashing process is the generation of a unique salt for each password. A salt is a random value that is combined with the password before hashing. This ensures that even if two users have the same password, their resulting hash values will differ due to the unique salt applied.</p>
<ul>
<li>Key stretching</li>
</ul>
<p>bcrypt employs a process called key stretching to slow down the hashing process. Key stretching involves repeatedly applying a cryptographic hash function multiple times. This deliberate slowdown is intentional, as it thwarts brute-force attacks by forcing attackers to spend significantly more time and computational resources trying various password combinations. The number of times the hash function is iterated is controlled by a parameter known as the "work factor" or "cost factor". A higher work factor increases the time and resources required to compute the hash, thereby enhancing the security of the hashed passwords.</p>
<ul>
<li>Password hashing</li>
</ul>
<p>Once the salt is generated and the work factor is determined, bcrypt combines these values with the user's password and passes them through the Blowfish cipher. The result is a cryptographic hash that represents the password and the associated salt.
The resulting hash is a fixed-length value, which means that regardless of the length of the original password, the hash length remains constant. This is a valuable property for securely storing and comparing password hashes.</p>
<ul>
<li>Verification process</li>
</ul>
<p>When a user attempts to log in, the application retrieves the stored hash from the database and applies the same salt and work factor during the verification process. The user's provided password is hashed using these parameters, and the resulting hash is compared with the stored hash. If they match, the provided password is correct; otherwise, authentication fails.
By incorporating salts, key stretching, and a cryptographically secure hashing algorithm, bcrypt ensures that password hashes are resistant to various types of attacks, providing a robust defense against unauthorized access.</p>
<h2 id="integrating-bcrypt-into-node-js-applications">Integrating bcrypt into Node.js applications</h2>
<p>Now that we have a solid grasp of bcrypt's inner workings, let's explore how to integrate bcrypt into your Node.js applications for secure password hashing. The process can be broken down into a few simple steps.</p>
<p>First, install the bcrypt library using npm or yarn. Open your terminal and run the following command:</p>
<div class="highlight"><pre class="highlight shell"><code>npm <span class="nb">install </span>bcrypt
</code></pre></div>
<p>This will install the bcrypt package and make it available for use in your Node.js project.</p>
<p><strong>Hashing a password:</strong></p>
<p>To hash a user's password during registration, follow these steps:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">bcrypt</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">bcrypt</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">saltRounds</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span> <span class="c1">// You can adjust this value as needed</span>
<span class="kd">const</span> <span class="nx">plainPassword</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">user_password</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">bcrypt</span><span class="p">.</span><span class="nx">genSalt</span><span class="p">(</span><span class="nx">saltRounds</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">salt</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">bcrypt</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="nx">plainPassword</span><span class="p">,</span> <span class="nx">salt</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">hash</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">err</span><span class="p">;</span>
<span class="c1">// Store the 'hash' in your database</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div>
<p>Adjust the saltRounds value according to the desired level of security. Higher values will result in slower hash generation.</p>
<p><strong>Verifying passwords:</strong>
When verifying a user's login attempt, use bcrypt to compare the stored hash with the provided password:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">bcrypt</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">bcrypt</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">storedHash</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">stored_hash_from_database</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">userProvidedPassword</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">user_input_password</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">bcrypt</span><span class="p">.</span><span class="nx">compare</span><span class="p">(</span><span class="nx">userProvidedPassword</span><span class="p">,</span> <span class="nx">storedHash</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">err</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">result</span> <span class="o">===</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Passwords match, grant access</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="c1">// Passwords do not match, deny access</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div>
<p>By following these steps, you can seamlessly integrate bcrypt into your Node.js application, ensuring that user passwords are securely hashed and verified.</p>
<h2 id="practical-implementation-of-bcrypt-in-a-node-js-application">Practical implementation of bcrypt in a Node.js application</h2>
<p>Now that we have a solid theoretical understanding of bcrypt, let's dive into a practical example of how to implement bcrypt for secure password hashing in a real-world Node.js application. For this example, we'll consider a simple user authentication system using Node.js, Express, and MongoDB. We'll walk through the process of registering users with hashed passwords, verifying passwords during login, and applying best practices for enhanced security.</p>
<h3 id="set-up-the-project">Set up the project</h3>
<p>Create a new directory for your project and navigate to it in your terminal. Run <code>npm init</code> to initialize a new Node.js project and follow the prompts to set up your <code>package.json</code>.</p>
<h3 id="install-dependencies">Install dependencies</h3>
<p>Install the necessary packages for your project:</p>
<div class="highlight"><pre class="highlight shell"><code>npm <span class="nb">install </span>express mongoose bcrypt
</code></pre></div>
<h3 id="create-the-express-application">Create the Express application</h3>
<p>Create a new file, <code>app.js</code>, and set up your Express application:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">mongoose</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">mongoose</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">bcrypt</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">bcrypt</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
<span class="c1">// Set up middleware and routes here</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">3000</span><span class="p">;</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Server started on port </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<h3 id="set-up-mongodb">Set up MongoDB</h3>
<p>Connect to your MongoDB database using Mongoose:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="nx">mongoose</span><span class="p">.</span><span class="nx">connect</span><span class="p">(</span><span class="dl">'</span><span class="s1">mongodb://localhost:27017/myapp</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">useNewUrlParser</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">useUnifiedTopology</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">useCreateIndex</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">connection</span><span class="p">;</span>
<span class="nx">db</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="nx">console</span><span class="p">,</span> <span class="dl">'</span><span class="s1">MongoDB connection error:</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">db</span><span class="p">.</span><span class="nx">once</span><span class="p">(</span><span class="dl">'</span><span class="s1">open</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Connected to MongoDB</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<h3 id="create-a-user-schema-and-model">Create a user schema and model</h3>
<p>Define a user schema and create a model using Mongoose:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">userSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">Schema</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="na">unique</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span>
<span class="na">password</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">User</span> <span class="o">=</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">model</span><span class="p">(</span><span class="dl">'</span><span class="s1">User</span><span class="dl">'</span><span class="p">,</span> <span class="nx">userSchema</span><span class="p">);</span>
</code></pre></div>
<h3 id="register-users-with-hashed-passwords">Register users with hashed passwords</h3>
<p>Implement the registration route to hash passwords and save users:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="nx">app</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/register</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">password</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">saltRounds</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">hashedPassword</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">bcrypt</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="nx">saltRounds</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">newUser</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">({</span> <span class="nx">username</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="nx">hashedPassword</span> <span class="p">});</span>
<span class="k">await</span> <span class="nx">newUser</span><span class="p">.</span><span class="nx">save</span><span class="p">();</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">201</span><span class="p">).</span><span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">User registered successfully</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">500</span><span class="p">).</span><span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">An error occurred</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div>
<h3 id="verify-passwords-during-login">Verify passwords during login</h3>
<p>Implement the login route to verify passwords:</p>
<div class="highlight"><pre class="highlight javascript"><code><span class="nx">app</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/login</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">password</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">User</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span> <span class="nx">username</span> <span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">404</span><span class="p">).</span><span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">User not found</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">passwordMatch</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">bcrypt</span><span class="p">.</span><span class="nx">compare</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">password</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">passwordMatch</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">Login successful</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">401</span><span class="p">).</span><span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">Invalid credentials</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">500</span><span class="p">).</span><span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">An error occurred</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div>
<h2 id="best-practices-for-using-bcrypt-in-node-js">Best practices for using bcrypt in Node.js</h2>
<p>While bcrypt is a powerful tool for securing passwords in Node.js applications, there are several best practices you should keep in mind to ensure it’s used effectively and responsibly. Let's delve into these practices to maximize the security of your application:</p>
<ol>
<li><p><strong>Choose an appropriate salt rounds value:</strong></p>
<p>The <code>saltRounds</code> parameter determines the computational cost of hashing a password. A higher value increases the time it takes to hash a password, thereby making brute-force attacks more difficult. However, this also means more processing time, so strike a balance between security and performance. The most common value is around 10, but you might need to adjust it based on your application's requirements.</p></li>
<li><p><strong>Use a secure random number generator:</strong></p>
<p>The quality of the salt is crucial. Ensure you're using a secure random number generator to create the salt. Node.js provides the <code>crypto</code> module, which offers strong cryptographic primitives, including random number generation.</p></li>
<li><p><strong>Update bcrypt regularly:</strong></p>
<p>Like any software library, bcrypt could have vulnerabilities that are discovered over time. Make sure to keep your dependencies up to date and monitor the bcrypt project for any security updates. Regularly updating the library in your project helps you stay protected against known vulnerabilities.</p></li>
<li><p><strong>Use HTTPS and Other security measures:</strong></p>
<p>While bcrypt is a robust method for password hashing, it's not the only aspect of securing user data. Utilize HTTPS to encrypt data in transit and follow other security best practices, such as input validation, to prevent other attack vectors.</p></li>
<li><p><strong>Consider two-factor authentication:</strong></p>
<p>Two-factor authentication (2FA) adds an extra layer of security by requiring users to provide a second piece of information, such as a code sent to their smartphone, in addition to their password. Implementing 2FA can significantly enhance the security of user accounts.</p></li>
<li><p><strong>Implement account lockout:</strong></p>
<p>To protect against brute-force attacks, implement account lockout mechanisms that temporarily lock an account after a certain number of failed login attempts. This prevents attackers from making unlimited login attempts, rendering brute-force attacks ineffective.</p></li>
<li><p><strong>Routinely audit and monitor:</strong></p>
<p>Routinely audit your application's security practices and monitor your logs for any suspicious activity. This proactive approach helps identify and address potential security breaches before they escalate.</p></li>
<li><p><strong>Use libraries with active maintenance:</strong></p>
<p>While bcrypt is widely used and trusted, it's important to use libraries that have active maintenance and a responsive development community. This ensures that any potential issues are addressed promptly.</p></li>
</ol>
<h2 id="strengthening-security-with-bcrypt-in-node-js">Strengthening security with bcrypt in Node.js</h2>
<p>bcrypt is a crucial tool for enhancing password security in Node.js applications. We've explored its core processes, from generating salts to creating secure hashes, and explained how to practically implement it for user authentication. Remembering best practices, such as setting strong salt rounds and staying updated, ensures the effective protection of user data. By using bcrypt, we can confidently safeguard passwords, bolster application security, and maintain user confidence in today's digital environment.</p>
Processes and Artisan commands in Laravelhttps://www.honeybadger.io/blog/laravel-artisan-processes/2024-01-23T00:00:00+00:002024-03-19T01:26:39+00:00Ashley AllenRead about how to create and test commands to interact with your Laravel application and server. Discover new tips and tricks for writing your own Artisan commands, and how to use Laravel 10's Process facade.<p>The command-line interface (CLI) can be a powerful tool for developers. You can use it as part of your development workflow to add new features to your application and to perform tasks in a production environment. Laravel allows you to create "Artisan commands" to add bespoke functionality to your application. It also provides a <code>Process</code> facade that you can use to run OS (operating system) processes from your Laravel application to perform tasks such as running custom shell scripts.</p>
<p>In this article, we'll explore what Artisan commands are and provide tips and tricks for creating and testing them. We'll also look at how to run operating system (OS) processes from your Laravel application and test they are called correctly.</p>
<h2 id="what-are-artisan-commands">What are Artisan commands?</h2>
<p>Artisan commands can be run from the CLI to perform many tasks on a Laravel app. They can allow a more streamlined development process and be used to perform tasks in a production environment.</p>
<p>As a Laravel developer, you'll have likely already used some built-in Artisan commands, such as <code>php artisan make:model</code>, <code>php artisan migrate</code>, and <code>php artisan down</code>.</p>
<p>For example, some Artisan commands can be used as part of the development process, such as <code>php artisan make:model</code> and <code>php artisan make:controller</code>. Typically, these wouldn't be run in a production environment and are used purely to speed up the development process by creating boilerplate files.</p>
<p>Some Artisan commands can be used to perform tasks in a production environment, such as <code>php artisan migrate</code> and <code>php artisan down</code>. These are used to perform tasks, such as running database migrations and taking your application offline while you perform maintenance or roll out an update.</p>
<p>Thus, Artisan commands can be used to perform a variety of tasks and can be used in both development and production environments.</p>
<h2 id="creating-your-own-artisan-commands">Creating your own Artisan commands</h2>
<p>Now that we have a better understanding of what Artisan commands are, let's look at how we can create our own.</p>
<h3 id="getting-input-from-users">Getting input from users</h3>
<p>To give some examples of what we can do with Artisan commands, let's take a look at a common use-case for them that you may across in your own projects: creating a new super admin in the database. In this example, we need the following pieces of information to create a new super admin:</p>
<ul>
<li>Name</li>
<li>Email address</li>
<li>Password</li>
</ul>
<p>Let's create the command to do this. We'll call the command <code>CreateSuperAdmin</code> and create it by running the following command:</p>
<div class="highlight"><pre class="highlight plaintext"><code>php artisan make:command CreateSuperAdmin
</code></pre></div>
<p>This command will create a new <code>app/Console/Commands/CreateSuperAdmin.php</code> file. We'll assume that we have access to a <code>createSuperAdmin</code> method in a <code>UserService</code> class. For the purposes of this article, we don't need to know what it does or how it works since we're focused on how the commands work.</p>
<p>The command class created by the <code>make:command</code> command will look something like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">CreateSuperAdmin</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="cd">/**
* The name and signature of the console command.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-super-admin'</span><span class="p">;</span>
<span class="cd">/**
* The console command description.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Command description'</span><span class="p">;</span>
<span class="cd">/**
* Execute the console command.
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="c1">//</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Now we want to add our arguments to the command so that we can accept the name, email, and password for each new user. We can do this by updating the <code>signature</code> property of the command class. The <code>signature</code> property is used to define the name of the command, the arguments, and the options that the command accepts.</p>
<p>The <code>signature</code> property should look something like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-super-admin {--email=} {--password=} {--name=}'</span><span class="p">;</span>
</code></pre></div>
<p>We may also want to add an option to the command to allow the user to specify whether an email should be sent to confirm the account. By default, we'll assume the user shouldn't be sent the email. To add this option to the code, we can update the <code>signature</code> property to look like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}'</span><span class="p">;</span>
</code></pre></div>
<p>It's important to also update the command's <code>description</code> property to describe what it does. This will be displayed when the user runs the <code>php artisan list</code> or <code>php artisan help</code> commands.</p>
<p>Now that we have our options configured to accept input, we can pass these options to our <code>createSuperAdmin</code> method. Let's take a look at what our command class will look like now:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">App\Services\UserService</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">CreateSuperAdmin</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="cd">/**
* The name and signature of the console command.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}'</span><span class="p">;</span>
<span class="cd">/**
* The console command description.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Store a new super admin in the database'</span><span class="p">;</span>
<span class="cd">/**
* Execute the console command.
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">UserService</span> <span class="nv">$userService</span><span class="p">):</span> <span class="kt">int</span>
<span class="p">{</span>
<span class="nv">$userService</span><span class="o">-></span><span class="nf">createSuperAdmin</span><span class="p">(</span>
<span class="n">email</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'email'</span><span class="p">),</span>
<span class="n">password</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'password'</span><span class="p">),</span>
<span class="n">name</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'name'</span><span class="p">),</span>
<span class="n">sendEmail</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'send-email'</span><span class="p">),</span>
<span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">components</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'Super admin created successfully!'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">self</span><span class="o">::</span><span class="no">SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Our command should now be ready to run. We can run it using the following command:</p>
<div class="highlight"><pre class="highlight plaintext"><code>php artisan app:create-super-admin --email="hello@example.com" --name="John Doe" --password="password" --send-email
</code></pre></div>
<p>If we wanted to take this a step further, we may also want to add questions to the command so that the user can be prompted to enter the information if they don't provide it as an argument or option. This can provide a friendly user experience for developers, especially if they are new to using the command or if it has several arguments and options.</p>
<p>If we wanted to update our command to use questions, our command may now look something like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">App\Services\UserService</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">CreateSuperAdmin</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="cd">/**
* The name and signature of the console command.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}'</span><span class="p">;</span>
<span class="cd">/**
* The console command description.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Store a new super admin in the database'</span><span class="p">;</span>
<span class="cd">/**
* Execute the console command.
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">UserService</span> <span class="nv">$userService</span><span class="p">):</span> <span class="kt">int</span>
<span class="p">{</span>
<span class="nv">$userService</span><span class="o">-></span><span class="nf">createSuperAdmin</span><span class="p">(</span>
<span class="n">email</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getInput</span><span class="p">(</span><span class="s1">'email'</span><span class="p">),</span>
<span class="n">password</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getInput</span><span class="p">(</span><span class="s1">'password'</span><span class="p">),</span>
<span class="n">name</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getInput</span><span class="p">(</span><span class="s1">'name'</span><span class="p">),</span>
<span class="n">sendEmail</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getInput</span><span class="p">(</span><span class="s1">'send-email'</span><span class="p">),</span>
<span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">components</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'Super admin created successfully!'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">self</span><span class="o">::</span><span class="no">SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getInput</span><span class="p">(</span><span class="kt">string</span> <span class="nv">$inputKey</span><span class="p">):</span> <span class="kt">string</span>
<span class="p">{</span>
<span class="k">return</span> <span class="k">match</span><span class="p">(</span><span class="nv">$inputKey</span><span class="p">)</span> <span class="p">{</span>
<span class="s1">'email'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'email'</span><span class="p">)</span> <span class="o">??</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">ask</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">),</span>
<span class="s1">'password'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'password'</span><span class="p">)</span> <span class="o">??</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">secret</span><span class="p">(</span><span class="s1">'Password'</span><span class="p">),</span>
<span class="s1">'name'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'name'</span><span class="p">)</span> <span class="o">??</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">ask</span><span class="p">(</span><span class="s1">'Name'</span><span class="p">),</span>
<span class="s1">'send-email'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'send-email'</span><span class="p">)</span> <span class="o">===</span> <span class="kc">true</span>
<span class="o">?</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'send-email'</span><span class="p">)</span>
<span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">confirm</span><span class="p">(</span><span class="s1">'Send email?'</span><span class="p">),</span>
<span class="k">default</span> <span class="o">=></span> <span class="kc">null</span><span class="p">,</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>As you can see, we've added a new <code>getInput</code> method to the command class. Within this method, we're checking whether an argument has been passed to the command. If it hasn't, we prompt the user for input. You may have also noticed that we've used the <code>secret</code> method for getting the new password. This method is used so that we can hide the password from the terminal output. If we didn't use the <code>secret</code> method, the password would be displayed in the terminal output. Similarly, we've also used the <code>confirm</code> method for determining whether to send an email to the new user. This will prompt the user with a <code>yes</code> or <code>no</code> question, so it's a good way to get a Boolean value from the user rather than using the <code>ask</code> method.</p>
<p>Running the command with only the <code>email</code> argument would result in the following output:</p>
<div class="highlight"><pre class="highlight plaintext"><code>❯ php artisan app:create-super-admin --email="hello@example.com"
Password:
>
Name:
> Joe Smith
Send email? (yes/no) [no]:
> yes
</code></pre></div>
<h3 id="anticipating-input">Anticipating input</h3>
<p>If you're building a command for the user and providing multiple options to choose from a known data set (e.g., rows in the database), you may sometimes want to anticipate the user's input. By doing this, it can suggest autocompleting options for the user to choose from.</p>
<p>For example, let's imagine that you have a command that can be used to create a new user and allows you to specify the user's role. You may want to anticipate the possible roles from which the user may choose. You can do this by using the <code>anticipate</code> method:</p>
<div class="highlight"><pre class="highlight php"><code><span class="nv">$roles</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'super-admin'</span><span class="p">,</span>
<span class="s1">'admin'</span><span class="p">,</span>
<span class="s1">'manager'</span><span class="p">,</span>
<span class="s1">'user'</span><span class="p">,</span>
<span class="p">];</span>
<span class="nv">$role</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">anticipate</span><span class="p">(</span><span class="s1">'What is the role of the new user?'</span><span class="p">,</span> <span class="nv">$roles</span><span class="p">);</span>
</code></pre></div>
<p>When the user is prompted to choose the role, the command will try to autocomplete the field. For instance, if the user started typing <code>sup</code>, the command would suggest <code>er-admin</code> as the autocomplete.</p>
<p>Running the command would result in the following output:</p>
<div class="highlight"><pre class="highlight plaintext"><code>What is the role of the new user?:
> super-admin
</code></pre></div>
<p>It's important to remember that the <code>anticipate</code> method is only providing a hint to the user, so they can still enter any value they'd like. Therefore, all input from the method must still be validated to ensure it can be used.</p>
<h3 id="multiple-arguments-and-choice-inputs">Multiple arguments and choice inputs</h3>
<p>There may be times when you're building an Artisan command and want to enable users to enter multiple options from a list. If you've used Laravel Sail, you'll have already used this feature when you run the <code>php artisan sail:install</code> command and are asked to choose the various services you want to install.</p>
<p>You can allow multiple inputs to be passed as arguments to the command. For example, if we wanted to create a command that can be used to install some services for our local development environment, we could allow the user to pass multiple services as arguments to the command:</p>
<div class="highlight"><pre class="highlight php"><code><span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:install-services {services?*}'</span><span class="p">;</span>
</code></pre></div>
<p>We could now call this command like so to install the <code>mysql</code> and <code>redis</code> services:</p>
<div class="highlight"><pre class="highlight plaintext"><code>php artisan app:install-services mysql redis
</code></pre></div>
<p>Within our Artisan command, <code>$this->argument('services')</code> would return an array containing two items: <code>mysql</code> and <code>redis</code>.</p>
<p>We could also add this functionality to our command to display the options to the user if they don't provide any arguments by using the <code>choice</code> method:</p>
<div class="highlight"><pre class="highlight php"><code><span class="nv">$installableServices</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'mysql'</span><span class="p">,</span>
<span class="s1">'redis'</span><span class="p">,</span>
<span class="s1">'mailpit'</span><span class="p">,</span>
<span class="p">];</span>
<span class="nv">$services</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">choice</span><span class="p">(</span>
<span class="n">question</span><span class="o">:</span> <span class="s1">'Which services do you want to install?'</span><span class="p">,</span>
<span class="n">choices</span><span class="o">:</span> <span class="nv">$installableServices</span><span class="p">,</span>
<span class="n">multiple</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="p">);</span>
</code></pre></div>
<p>Running this command without any arguments would now display the following output:</p>
<div class="highlight"><pre class="highlight plaintext"><code>Which services do you want to install?:
[0] mysql
[1] redis
[2] mailpit
> mysql,redis
</code></pre></div>
<p>Using the <code>choice</code> method, we can allow the user to select multiple options from a list. The method provides auto-completion for the user, similar to the <code>anticipate</code> method. It also comes in handy because it will only allow the user to select options from the list. If the user tries to enter an option that isn't on the list, they'll be prompted to try again. Hence, it can act as a form of validation for your users' input.</p>
<h3 id="input-validation">Input validation</h3>
<p>Similar to how you would validate the input for an HTTP request, you may also want to validate the input of your Artisan commands. By doing this, you can ensure that the input is correct and can be passed to other parts of your application's code.</p>
<p>Let's take a look at a possible way to validate the input. We'll start by reviewing the code, and then I'll break it down afterwards.</p>
<p>To validate your input, you could do something like so:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">App\Services\UserService</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Validator</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\MessageBag</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">InvalidArgumentException</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">CreateSuperAdmin</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="cd">/**
* The name and signature of the console command.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}'</span><span class="p">;</span>
<span class="cd">/**
* The console command description.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Store a new super admin in the database'</span><span class="p">;</span>
<span class="k">private</span> <span class="kt">bool</span> <span class="nv">$inputIsValid</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="cd">/**
* Execute the console command.
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">UserService</span> <span class="nv">$userService</span><span class="p">):</span> <span class="kt">int</span>
<span class="p">{</span>
<span class="nv">$input</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">validateInput</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$this</span><span class="o">-></span><span class="n">inputIsValid</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">self</span><span class="o">::</span><span class="no">FAILURE</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$userService</span><span class="o">-></span><span class="nf">createSuperAdmin</span><span class="p">(</span>
<span class="n">email</span><span class="o">:</span> <span class="nv">$input</span><span class="p">[</span><span class="s1">'email'</span><span class="p">],</span>
<span class="n">password</span><span class="o">:</span> <span class="nv">$input</span><span class="p">[</span><span class="s1">'password'</span><span class="p">],</span>
<span class="n">name</span><span class="o">:</span> <span class="nv">$input</span><span class="p">[</span><span class="s1">'name'</span><span class="p">],</span>
<span class="n">sendEmail</span><span class="o">:</span> <span class="nv">$input</span><span class="p">[</span><span class="s1">'send-email'</span><span class="p">],</span>
<span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">components</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'Super admin created successfully!'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">self</span><span class="o">::</span><span class="no">SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="cd">/**
* Validate and return all the input from the command. If any of the input
* was invalid, an InvalidArgumentException will be thrown. We catch this
* and report it so it's still logged or sent to a bug-tracking system.
* But we don't display it to the console. Only the validation error
* messages will be displayed in the console.
*
* @return array
*/</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">validateInput</span><span class="p">():</span> <span class="kt">array</span>
<span class="p">{</span>
<span class="nv">$input</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">try</span> <span class="p">{</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nb">array_keys</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">rules</span><span class="p">())</span> <span class="k">as</span> <span class="nv">$inputKey</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$input</span><span class="p">[</span><span class="nv">$inputKey</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">validated</span><span class="p">(</span><span class="nv">$inputKey</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nc">InvalidArgumentException</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">inputIsValid</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nf">report</span><span class="p">(</span><span class="nv">$e</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nv">$input</span><span class="p">;</span>
<span class="p">}</span>
<span class="cd">/**
* Validate the input and then return it. If the input is invalid, we will
* display the validation messages and then throw an exception.
*
* @param string $inputKey
* @return string
*/</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">validated</span><span class="p">(</span><span class="kt">string</span> <span class="nv">$inputKey</span><span class="p">):</span> <span class="kt">string</span>
<span class="p">{</span>
<span class="nv">$input</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getInput</span><span class="p">(</span><span class="nv">$inputKey</span><span class="p">);</span>
<span class="nv">$validator</span> <span class="o">=</span> <span class="nc">Validator</span><span class="o">::</span><span class="nf">make</span><span class="p">(</span>
<span class="n">data</span><span class="o">:</span> <span class="p">[</span><span class="nv">$inputKey</span> <span class="o">=></span> <span class="nv">$input</span><span class="p">],</span>
<span class="n">rules</span><span class="o">:</span> <span class="p">[</span><span class="nv">$inputKey</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">rules</span><span class="p">()[</span><span class="nv">$inputKey</span><span class="p">]]</span>
<span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$validator</span><span class="o">-></span><span class="nf">passes</span><span class="p">())</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$input</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">handleInvalidData</span><span class="p">(</span><span class="nv">$validator</span><span class="o">-></span><span class="nf">errors</span><span class="p">());</span>
<span class="p">}</span>
<span class="cd">/**
* Loop through each of the error messages and output them to the console.
* Then throw an exception so we can prevent the rest of the command
* from running. We will catch this in the "validateInput" method.
*
* @param MessageBag $errors
* @return void
*/</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">handleInvalidData</span><span class="p">(</span><span class="kt">MessageBag</span> <span class="nv">$errors</span><span class="p">):</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$errors</span><span class="o">-></span><span class="nf">all</span><span class="p">()</span> <span class="k">as</span> <span class="nv">$error</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="n">components</span><span class="o">-></span><span class="nf">error</span><span class="p">(</span><span class="nv">$error</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nc">InvalidArgumentException</span><span class="p">();</span>
<span class="p">}</span>
<span class="cd">/**
* Define the rules used to validate the input.
*
* @return array<string,string>
*/</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">rules</span><span class="p">():</span> <span class="kt">array</span>
<span class="p">{</span>
<span class="k">return</span> <span class="p">[</span>
<span class="s1">'email'</span> <span class="o">=></span> <span class="s1">'required|email'</span><span class="p">,</span>
<span class="s1">'password'</span> <span class="o">=></span> <span class="s1">'required|min:8'</span><span class="p">,</span>
<span class="s1">'name'</span> <span class="o">=></span> <span class="s1">'required'</span><span class="p">,</span>
<span class="s1">'send-email'</span> <span class="o">=></span> <span class="s1">'boolean'</span><span class="p">,</span>
<span class="p">];</span>
<span class="p">}</span>
<span class="cd">/**
* Attempt to get the input from the command options. If the input wasn't passed
* to the command, ask the user for the input.
*
* @param string $inputKey
* @return string|null
*/</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">getInput</span><span class="p">(</span><span class="kt">string</span> <span class="nv">$inputKey</span><span class="p">):</span> <span class="kt">?string</span>
<span class="p">{</span>
<span class="k">return</span> <span class="k">match</span><span class="p">(</span><span class="nv">$inputKey</span><span class="p">)</span> <span class="p">{</span>
<span class="s1">'email'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'email'</span><span class="p">)</span> <span class="o">??</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">ask</span><span class="p">(</span><span class="s1">'Email'</span><span class="p">),</span>
<span class="s1">'password'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'password'</span><span class="p">)</span> <span class="o">??</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">secret</span><span class="p">(</span><span class="s1">'Password'</span><span class="p">),</span>
<span class="s1">'name'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'name'</span><span class="p">)</span> <span class="o">??</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">ask</span><span class="p">(</span><span class="s1">'Name'</span><span class="p">),</span>
<span class="s1">'send-email'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'send-email'</span><span class="p">)</span> <span class="o">===</span> <span class="kc">true</span>
<span class="o">?</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'send-email'</span><span class="p">)</span>
<span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">confirm</span><span class="p">(</span><span class="s1">'Send email?'</span><span class="p">),</span>
<span class="k">default</span> <span class="o">=></span> <span class="kc">null</span><span class="p">,</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>Let's break down the above code.</p>
<p>First, we're calling a <code>validateInput</code> method before we try to do anything with the input. This method loops through every input key that we've specified in our <code>rules</code> method and calls the <code>validated</code> method. The array returned from the <code>validateInput</code> method will only contain validated data.</p>
<p>If any of the input is invalid, the necessary error messages will be displayed on the page. You may have noticed that we're also catching any <code>InvalidArgumentException</code> exceptions thrown. This is done so that we can still log or send the exception to a bug-tracking system, but without displaying the exception message in the console. Thus, we can keep the console output neat by only showing the validation error messages.</p>
<p>Running the above code and providing an invalid email address will result in the following output:</p>
<div class="highlight"><pre class="highlight plaintext"><code>❯ php artisan app:create-super-admin
Email:
> INVALID
ERROR The email field must be a valid email address.
</code></pre></div>
<h3 id="hiding-commands-from-the-list">Hiding commands from the list</h3>
<p>Depending on the type of command you're building, you may want to hide it from the <code>php artisan list</code> command that displays all your app's available commands. This is useful if you're building a command that is only intended to run once, such as an installation command for a package.</p>
<p>To hide a command from the list, you can use the <code>setHidden</code> property to set the value the command's <code>hidden</code> property to <code>true</code>. For example, if you're building an installation command as part of a package that publishes some assets, you may want to check whether those assets already exist in the filesystem. If they do, you can probably assume that the command has already been run once and doesn't need to be displayed in the <code>list</code> command output.</p>
<p>Let's take a look at how we can do this. We'll imagine that our package publishes a <code>my-new-package.php</code> config file at the time of installation. If this file exists, we'll hide the command from the list. Our command's code may look something like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">PackageInstall</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:package-install'</span><span class="p">;</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Install the package and publish the assets'</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">parent</span><span class="o">::</span><span class="nf">__construct</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">file_exists</span><span class="p">(</span><span class="nf">config_path</span><span class="p">(</span><span class="s1">'my-new-package.php'</span><span class="p">)))</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">setHidden</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">// Run the command here as usual...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>It's worth noting that hiding a command doesn't stop the command from being able to be run. If someone knows the name of the command, they can still run it.</p>
<h3 id="command-help">Command help</h3>
<p>When building your commands, it's crucial to use obvious names for your arguments and options. This will make it easier for other developers to understand what the command does and how to use it.</p>
<p>However, there may be times when you want to provide additional help information so they can be displayed in the console by running the <code>php artisan help</code> command.</p>
<p>For instance, if we wanted to take our <code>CreateSuperAdmin</code> example from earlier in this guide, we could update the signature to the following:</p>
<div class="highlight"><pre class="highlight php"><code><span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-super-admin
{--email= : The email address of the new user}
{--password= : The password of the new user}
{--name= : The name of the new user}
{--send-email : Send a welcome email after creating the user}'</span><span class="p">;</span>
</code></pre></div>
<p>Now if we were to run <code>php artisan help app:create-super-admin</code>, we would see the following output:</p>
<div class="highlight"><pre class="highlight plaintext"><code>❯ php artisan help app:create-super-admin
Description:
Store a new super admin in the database
Usage:
app:create-super-admin [options]
Options:
--email[=EMAIL] The email address of the new user
--password[=PASSWORD] The password of the new user
--name[=NAME] The name of the new user
--send-email Send a welcome email after creating the user
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
</code></pre></div>
<h3 id="automate-tasks-using-the-scheduler">Automate tasks using the scheduler</h3>
<p>Artisan commands also provide a way to automate tasks in your application.</p>
<p>For example, let's say that on the first day of every month, you want to build a PDF report for your users' activity from the previous month and email it to them. To automate this process, you could create a custom Artisan command and add it to your app's "scheduler". Assuming you had a scheduler set up on your server, this command would run automatically on the first day of each month and send the report to your users.</p>
<p>For the purposes of this article, we won't be covering how to set up a scheduler on your server. However, if you're interested in learning more about how to do this, you can check out the <a href="https://laravel.com/docs/10.x/scheduling#running-the-scheduler">Laravel documentation</a>. In its most basic terms, though, the scheduler is a cron job that runs on your server and is called once a minute. The cron job runs the following command:</p>
<div class="highlight"><pre class="highlight plaintext"><code>php /path/to/artisan schedule:run >> /dev/null 2>&1
</code></pre></div>
<p>It's essentially just calling the <code>php artisan schedule:run</code> command for your project, which allows Laravel to handle whether the scheduled commands are ready to be run. The <code>>> /dev/null</code> part of the command is redirecting the output of the command to <code>/dev/null</code> which discards it, so it's not displayed. The <code>2>&1</code> part of the command is redirecting the error output of the command to the standard output. This is used so that any errors occurring when the command is run are also discarded.</p>
<p>Now, let's take a look at how to add a command to the scheduler in Laravel.</p>
<p>Let's imagine we want to implement our example from above and email our users on the first day of each month. To do this, we'll need to create a <code>SendMonthlyReport</code> Artisan command:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">SendMonthlyReport</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:send-monthly-report'</span><span class="p">;</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Send monthly report to each user'</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="c1">// Send the monthly reports here...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>After creating your command, we can then add it to your app's scheduler. To do this, we'll need to register it in the <code>schedule</code> method of the <code>app/Console/Kernel.php</code> file. Your <code>Kernel</code> class may look something like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">App\Console\Commands\SendMonthlyReport</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Scheduling\Schedule</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Foundation\Console\Kernel</span> <span class="k">as</span> <span class="nc">ConsoleKernel</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">Kernel</span> <span class="kd">extends</span> <span class="nc">ConsoleKernel</span>
<span class="p">{</span>
<span class="cd">/**
* Define the application's command schedule.
*/</span>
<span class="k">protected</span> <span class="k">function</span> <span class="n">schedule</span><span class="p">(</span><span class="kt">Schedule</span> <span class="nv">$schedule</span><span class="p">):</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nv">$schedule</span><span class="o">-></span><span class="nf">command</span><span class="p">(</span><span class="s1">'app:send-monthly-report'</span><span class="p">)</span><span class="o">-></span><span class="nf">monthly</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div>
<p>If your scheduler is set up correctly on your server, this command should now automatically run at the beginning of each month.</p>
<p>A top tip when building automated commands like this is to add the functionality to pass arguments and options to the command. Sometimes, the command may fail when it's run automatically. For example, a user might not receive their monthly report, so by adding the options and arguments, you could manually rerun the command just for one user rather than everyone in the system. Although you may feel you don't need this functionality, it is something that has always come in handy with projects I've worked on.</p>
<h2 id="testing-your-artisan-commands">Testing your Artisan commands</h2>
<p>Like any other piece of code you write, it's important you test your Artisan commands. This is particularly true if you're going to be using them in a production environment or to automate tasks via the scheduler.</p>
<h3 id="asserting-output-from-commands">Asserting output from commands</h3>
<p>Typically, the easiest parts of your command to test are that they were run successfully and that they output the expected text.</p>
<p>Let's imagine that we have the following example command we want to test:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">App\Services\UserService</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">CreateUser</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-user {email} {--role=}'</span><span class="p">;</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Create a new user'</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">UserService</span> <span class="nv">$userService</span><span class="p">):</span> <span class="kt">int</span>
<span class="p">{</span>
<span class="nv">$userService</span><span class="o">-></span><span class="nf">createUser</span><span class="p">(</span>
<span class="n">email</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">argument</span><span class="p">(</span><span class="s1">'email'</span><span class="p">),</span>
<span class="n">role</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">option</span><span class="p">(</span><span class="s1">'role'</span><span class="p">),</span>
<span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'User created successfully!'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">self</span><span class="o">::</span><span class="no">SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>If we wanted to test that it was run successfully and that it outputs the expected text, we could do the following:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">Tests\Feature\Console\Commands</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Tests\TestCase</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">CreateUserTest</span> <span class="kd">extends</span> <span class="nc">TestCase</span>
<span class="p">{</span>
<span class="cd">/** @test */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">user_can_be_created</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">artisan</span><span class="p">(</span><span class="s1">'app:create-user'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'email'</span> <span class="o">=></span> <span class="s1">'hello@example.com'</span><span class="p">,</span>
<span class="s1">'--role'</span> <span class="o">=></span> <span class="s1">'super-admin'</span>
<span class="p">])</span>
<span class="o">-></span><span class="nf">expectsOutput</span><span class="p">(</span><span class="s1">'User created successfully!'</span><span class="p">)</span>
<span class="o">-></span><span class="nf">assertSuccessful</span><span class="p">();</span>
<span class="c1">// Run extra assertions to check the user was created with the correct role...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<h3 id="asserting-questions-are-asked">Asserting questions are asked</h3>
<p>If your application asks the user a question, you will need to specify the answer to the question in your test. This can be done by using the <code>expectsQuestion</code> method.</p>
<p>For example, let's imagine we want to test the following command:</p>
<div class="highlight"><pre class="highlight php"><code><span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:create-user {email}'</span><span class="p">;</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Create a new user'</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">UserService</span> <span class="nv">$userService</span><span class="p">):</span> <span class="kt">int</span>
<span class="p">{</span>
<span class="nv">$roles</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'super-admin'</span><span class="p">,</span>
<span class="s1">'admin'</span><span class="p">,</span>
<span class="s1">'manager'</span><span class="p">,</span>
<span class="s1">'user'</span><span class="p">,</span>
<span class="p">];</span>
<span class="nv">$role</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">choice</span><span class="p">(</span><span class="s1">'What is the role of the new user?'</span><span class="p">,</span> <span class="nv">$roles</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">in_array</span><span class="p">(</span><span class="nv">$role</span><span class="p">,</span> <span class="nv">$roles</span><span class="p">,</span> <span class="kc">true</span><span class="p">))</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">error</span><span class="p">(</span><span class="s1">'The role is invalid!'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">self</span><span class="o">::</span><span class="no">FAILURE</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$userService</span><span class="o">-></span><span class="nf">createUser</span><span class="p">(</span>
<span class="n">email</span><span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">argument</span><span class="p">(</span><span class="s1">'email'</span><span class="p">),</span>
<span class="n">role</span><span class="o">:</span> <span class="nv">$role</span><span class="p">,</span>
<span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'User created successfully!'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">self</span><span class="o">::</span><span class="no">SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>To test this command, we could do the following:</p>
<div class="highlight"><pre class="highlight php"><code><span class="cd">/** @test */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">user_can_be_created_with_choice_for_role</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">artisan</span><span class="p">(</span><span class="s1">'app:create-user'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'email'</span> <span class="o">=></span> <span class="s1">'hello@example.com'</span><span class="p">,</span>
<span class="p">])</span>
<span class="o">-></span><span class="nf">expectsQuestion</span><span class="p">(</span><span class="s1">'What is the role of the new user?'</span><span class="p">,</span> <span class="s1">'super-admin'</span><span class="p">)</span>
<span class="o">-></span><span class="nf">expectsOutput</span><span class="p">(</span><span class="s1">'User created successfully!'</span><span class="p">)</span>
<span class="o">-></span><span class="nf">assertSuccessful</span><span class="p">();</span>
<span class="c1">// Run extra assertions to check the user was created with the correct role...</span>
<span class="p">}</span>
<span class="cd">/** @test */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">error_is_returned_if_the_role_is_invalid</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">artisan</span><span class="p">(</span><span class="s1">'app:create-user'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'email'</span> <span class="o">=></span> <span class="s1">'hello@example.com'</span><span class="p">,</span>
<span class="p">])</span>
<span class="o">-></span><span class="nf">expectsQuestion</span><span class="p">(</span><span class="s1">'What is the role of the new user?'</span><span class="p">,</span> <span class="s1">'INVALID'</span><span class="p">)</span>
<span class="o">-></span><span class="nf">expectsOutput</span><span class="p">(</span><span class="s1">'The role is invalid!'</span><span class="p">)</span>
<span class="o">-></span><span class="nf">assertFailed</span><span class="p">();</span>
<span class="c1">// Run extra assertions to check the user wasn't created...</span>
<span class="p">}</span>
</code></pre></div>
<p>As you can see in the code above, we have one test to ensure the user is created successfully and another test to ensure that an error is returned if the role is invalid.</p>
<h2 id="running-os-processes-in-laravel">Running OS processes in Laravel</h2>
<p>So far, we've looked at how you can run interact with your Laravel application by running commands from the operating system. However, there may be times when you want to do the opposite and run commands on the operating system from your Laravel application.</p>
<p>This can be useful, for example, when running custom shell scripts, antivirus scans, or file converters.</p>
<p>Let's take a look at how we can add this functionality to our application by using the <code>Process</code> facade that was added in Laravel 10. We'll cover several of the features that you'd be most likely to use in your application. However, if you want to see all the available methods, you can check out the <a href="https://laravel.com/docs/10.x/processes">Laravel documentation</a>.</p>
<h3 id="running-processes">Running processes</h3>
<p>To run a command on the operating system, you can use the <code>run</code> method on the <code>Process</code> facade. Let's imagine that we have a shell script called <code>install.sh</code> in the root directory of our project. We can run this script from our code:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="nv">$process</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="s1">'./install.sh'</span><span class="p">);</span>
<span class="nv">$output</span> <span class="o">=</span> <span class="nv">$process</span><span class="o">-></span><span class="nf">output</span><span class="p">();</span>
</code></pre></div>
<p>To provide additional context, we may want to run this script from the <code>handle</code> method of an Artisan command that does several things (e.g., reading from the database or making API calls). We could call the shell script from our command:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">use</span> <span class="nc">Illuminate\Console\Command</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">RunInstallShellScript</span> <span class="kd">extends</span> <span class="nc">Command</span>
<span class="p">{</span>
<span class="cd">/**
* The name and signature of the console command.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$signature</span> <span class="o">=</span> <span class="s1">'app:run-install-shell-script'</span><span class="p">;</span>
<span class="cd">/**
* The console command description.
*
* @var string
*/</span>
<span class="k">protected</span> <span class="nv">$description</span> <span class="o">=</span> <span class="s1">'Install the package and publish the assets'</span><span class="p">;</span>
<span class="cd">/**
* Execute the console command.
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'Starting installation...'</span><span class="p">);</span>
<span class="nv">$process</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="s1">'./install.sh'</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="nv">$process</span><span class="o">-></span><span class="nf">output</span><span class="p">());</span>
<span class="c1">// Make calls to API here...</span>
<span class="c1">// Publish assets here...</span>
<span class="c1">// Add new rows to the database here...</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'Installation complete!'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>You may also notice that we've grabbed the output using the <code>output</code> method so that we can handle it in our Laravel application. For example, if the process was being run via an HTTP request, we may want to display the output on the page.</p>
<p>The commands typically have two types of output: standard output and error output. If we wanted to get any error output from the process, we'd need to use the <code>errorOutput</code> method instead.</p>
<h3 id="running-the-process-from-another-directory">Running the process from another directory</h3>
<p>By default, the <code>Process</code> facade will run the command in the same working directory as the PHP file that's being run. Typically, this means if you are running the process using an Artisan command, the process will be run in the root directory of your project because that's where the <code>artisan</code> file is located. However, if the process is being run from an HTTP controller, the process will be run in the <code>public</code> directory of your project because that's the entry point for the web server.</p>
<p>Therefore, if you can run the process from both the console and the web, not explicitly specifying the working directory may result in unexpected behavior and errors. One way to tackle this problem is to ensure you always use the <code>path</code> method where possible. This will ensure that the process is always run in the expected directory.</p>
<p>For example, let's imagine that we have a shell script called <code>custom-script.sh</code> in a <code>scripts/custom</code> directory in our project. To run this script, we could do the following:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="nv">$process</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">(</span><span class="s1">'/scripts/custom'</span><span class="p">))</span><span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="s1">'./install.sh'</span><span class="p">);</span>
<span class="nv">$output</span> <span class="o">=</span> <span class="nv">$process</span><span class="o">-></span><span class="nf">output</span><span class="p">();</span>
</code></pre></div>
<h3 id="specifying-the-process-timeout">Specifying the process timeout</h3>
<p>By default, the <code>Process</code> facade will allow processes to be run for a maximum of 60 seconds before it times out. This is to prevent processes from running indefinitely (e.g., if they get stuck in an infinite loop).</p>
<p>However, if you want to run a process that may take longer than the default timeout, you can specify a custom timeout in seconds by using the <code>timeout</code> method. For example, let's imagine that we want to run a process that may take up to 5 minutes (300 seconds) to complete:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="nv">$process</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">timeout</span><span class="p">(</span><span class="mi">300</span><span class="p">)</span><span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="s1">'./install.sh'</span><span class="p">);</span>
<span class="nv">$output</span> <span class="o">=</span> <span class="nv">$process</span><span class="o">-></span><span class="nf">output</span><span class="p">();</span>
</code></pre></div>
<h3 id="getting-the-output-in-real-time">Getting the output in real time</h3>
<p>Depending on the process you're executing, you may prefer to output the text from the process as it's running rather than waiting for the process to finish. You might want to do this if you have a long-running script (e.g., an installation script) where you want to see the steps the script is going through.</p>
<p>To do this, you can pass a closure as the second parameter of the <code>run</code> method to determine what to do with the output as it's received. For example, let's imagine that we have a shell script called <code>install.sh</code> and want to see the output in real-time rather than waiting until it's finished. We could run the script like so:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="nv">$process</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">run</span><span class="p">(</span><span class="s1">'./install.sh'</span><span class="p">,</span> <span class="k">function</span> <span class="p">(</span><span class="kt">string</span> <span class="nv">$type</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$output</span><span class="p">):</span> <span class="kt">void</span> <span class="p">{</span>
<span class="nv">$type</span> <span class="o">===</span> <span class="s1">'out'</span> <span class="o">?</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">line</span><span class="p">(</span><span class="nv">$output</span><span class="p">)</span> <span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">error</span><span class="p">(</span><span class="nv">$output</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div>
<h3 id="running-processes-asynchronously">Running processes asynchronously</h3>
<p>There may be times when you want to run a process and carry on running other code while it's running rather than waiting until it's complete. For example, you may want to run an installation script and write to the database or make some API calls while the installation is ongoing.</p>
<p>To do this, you can use the <code>start</code> method. Let's take a look at an example of how we might do this. Let's imagine that we have a shell script called <code>install.sh</code> and want to run some other installation steps while we wait for the shell script to finish running:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'Starting installation...'</span><span class="p">);</span>
<span class="nv">$extraCodeHasBeenRun</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nv">$process</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">start</span><span class="p">(</span><span class="s1">'./install.sh'</span><span class="p">);</span>
<span class="k">while</span> <span class="p">(</span><span class="nv">$process</span><span class="o">-></span><span class="nf">running</span><span class="p">())</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$extraCodeHasBeenRun</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$extraCodeHasBeenRun</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">runExtraCode</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nv">$result</span> <span class="o">=</span> <span class="nv">$process</span><span class="o">-></span><span class="nf">wait</span><span class="p">();</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="nv">$process</span><span class="o">-></span><span class="nf">output</span><span class="p">());</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1">'Installation complete!'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">runExtraCode</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="c1">// Make calls to API here...</span>
<span class="c1">// Publish assets here...</span>
<span class="c1">// Add new rows to the database here...</span>
<span class="p">}</span>
</code></pre></div>
<p>As you may have noticed, we're checking in the <code>while</code> loop that we haven't already started running the extra code. We do this because we don't want to run the extra code multiple times. We only want to run it once while the process is running.</p>
<p>After the process has finished running, we can get the result of the process (using the <code>output</code> method) and output it to the console.</p>
<h3 id="running-processes-concurrently">Running processes concurrently</h3>
<p>There may be times when you want to run multiple commands concurrently. For example, you may want to do this if you have a script that converts a file from one format to another. If you want to convert multiple files at once in bulk (and the script doesn't support multiple files), you may want to run the script for each file concurrently.</p>
<p>This is beneficial because you don't need to run the script for each file sequentially. For instance, if a script took five seconds to run for each file, and you had 3 files to convert, it would take fifteen seconds to run sequentially. However, if you ran the script concurrently, it would only take five seconds to run.</p>
<p>To do this you can use the <code>concurrently</code> method. Let's take a look at an example of how to use it.</p>
<p>We'll imagine that we want to run a <code>convert.sh</code> shell script for three files in a directory. For the purpose of this example, we'll hard code these filenames. However, in a real-world scenario, you'd likely get these filenames dynamically from the filesystem, database, or request.</p>
<p>If we run the scripts sequentially, our code may look something like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="c1">// 15 seconds to run...</span>
<span class="nv">$firstOutput</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="s1">'./convert.sh file-1.png'</span><span class="p">);</span>
<span class="nv">$secondOutput</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="s1">'./convert.sh file-1.png'</span><span class="p">);</span>
<span class="nv">$thirdOutput</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="s1">'./convert.sh file-1.png'</span><span class="p">);</span>
</code></pre></div>
<p>However, if we run the scripts concurrently, our code may look something like this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="c1">// 5 seconds to run...</span>
<span class="nv">$commands</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">concurrently</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="kt">Pool</span> <span class="nv">$pool</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$pool</span><span class="o">-></span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">command</span><span class="p">(</span><span class="s1">'./convert.sh file-1.png'</span><span class="p">);</span>
<span class="nv">$pool</span><span class="o">-></span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">command</span><span class="p">(</span><span class="s1">'./convert.sh file-2.png'</span><span class="p">);</span>
<span class="nv">$pool</span><span class="o">-></span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span><span class="o">-></span><span class="nf">command</span><span class="p">(</span><span class="s1">'./convert.sh file-3.png'</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$commands</span><span class="o">-></span><span class="nf">collect</span><span class="p">()</span> <span class="k">as</span> <span class="nv">$command</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="nv">$command</span><span class="o">-></span><span class="nf">output</span><span class="p">());</span>
<span class="p">}</span>
</code></pre></div>
<h2 id="testing-os-processes">Testing OS processes</h2>
<p>Like Artisan commands, you can also test that your OS processes are executed as expected. Let's take a look at some common things you may want to test.</p>
<p>To get started with testing our processes, let's imagine that we have a web route in our application that accepts a file and converts it to a different format. If the file passed is an image, we'll convert the file using a <code>convert-image.sh</code> script. If the file is a video, we'll convert it using <code>convert-video.sh</code>. We'll assume that we have an <code>isImage</code> method in our controller that will return <code>true</code> if the file is an image and <code>false</code> if it's a video.</p>
<p>For the purposes of this example, we're not going to worry about any validation or HTTP testing. We'll focus solely on testing the process. However, in a real-life project, you would want to ensure that your test covers all the possible scenarios.</p>
<p>We'll imagine that our controller can be reached via a POST request to <code>/file/convert</code> (with the route named <code>file.convert</code>) and that it looks something this:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">App\Http\Controllers</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Http\Request</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Http\UploadedFile</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Storage</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Str</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">ConvertFileController</span> <span class="kd">extends</span> <span class="nc">Controller</span>
<span class="p">{</span>
<span class="cd">/**
* Handle the incoming request.
*/</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">__invoke</span><span class="p">(</span><span class="kt">Request</span> <span class="nv">$request</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Temporarily the file so it can be converted.</span>
<span class="nv">$tempFilePath</span> <span class="o">=</span> <span class="s1">'tmp/'</span><span class="mf">.</span><span class="nc">Str</span><span class="o">::</span><span class="nf">random</span><span class="p">();</span>
<span class="nc">Storage</span><span class="o">::</span><span class="nf">put</span><span class="p">(</span>
<span class="nv">$tempFilePath</span><span class="p">,</span>
<span class="nv">$request</span><span class="o">-></span><span class="nb">file</span><span class="p">(</span><span class="s1">'uploaded_file'</span><span class="p">)</span>
<span class="p">);</span>
<span class="c1">// Determine which command to run.</span>
<span class="nv">$command</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">isImage</span><span class="p">(</span><span class="nv">$request</span><span class="o">-></span><span class="nb">file</span><span class="p">(</span><span class="s1">'uploaded_file'</span><span class="p">))</span>
<span class="o">?</span> <span class="s1">'convert-image.sh'</span>
<span class="o">:</span> <span class="s1">'convert-video.sh'</span><span class="p">;</span>
<span class="nv">$command</span> <span class="mf">.</span><span class="o">=</span> <span class="s1">' '</span><span class="mf">.</span><span class="nv">$tempFilePath</span><span class="p">;</span>
<span class="c1">// The conversion command.</span>
<span class="nv">$process</span> <span class="o">=</span> <span class="nc">Process</span><span class="o">::</span><span class="nf">timeout</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span>
<span class="o">-></span><span class="nf">path</span><span class="p">(</span><span class="nf">base_path</span><span class="p">())</span>
<span class="o">-></span><span class="nf">run</span><span class="p">(</span><span class="nv">$command</span><span class="p">);</span>
<span class="c1">// If the process fails, report the error.</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$process</span><span class="o">-></span><span class="nf">failed</span><span class="p">())</span> <span class="p">{</span>
<span class="c1">// Report the error here to the logs or bug tracking system...</span>
<span class="k">return</span> <span class="nf">response</span><span class="p">()</span><span class="o">-></span><span class="nf">json</span><span class="p">([</span>
<span class="s1">'message'</span> <span class="o">=></span> <span class="s1">'Something went wrong!'</span><span class="p">,</span>
<span class="p">],</span> <span class="mi">500</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nf">response</span><span class="p">()</span><span class="o">-></span><span class="nf">json</span><span class="p">([</span>
<span class="s1">'message'</span> <span class="o">=></span> <span class="s1">'File converted successfully!'</span><span class="p">,</span>
<span class="s1">'path'</span> <span class="o">=></span> <span class="nb">trim</span><span class="p">(</span><span class="nv">$process</span><span class="o">-></span><span class="nf">output</span><span class="p">()),</span>
<span class="p">]);</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div>
<p>To test that the processes are correctly dispatched, we may want to write the following tests:</p>
<div class="highlight"><pre class="highlight php"><code><span class="kn">namespace</span> <span class="nn">Tests\Feature\Http\Controllers</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Http\UploadedFile</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Process\PendingProcess</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Process</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Storage</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Illuminate\Support\Str</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Tests\TestCase</span><span class="p">;</span>
<span class="kd">class</span> <span class="nc">ConvertFileControllerTest</span> <span class="kd">extends</span> <span class="nc">TestCase</span>
<span class="p">{</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">setUp</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="k">parent</span><span class="o">::</span><span class="nf">setUp</span><span class="p">();</span>
<span class="nc">Storage</span><span class="o">::</span><span class="nf">fake</span><span class="p">();</span>
<span class="nc">Process</span><span class="o">::</span><span class="nf">preventStrayProcesses</span><span class="p">();</span>
<span class="c1">// Determine how the random strings should be built.</span>
<span class="c1">// We do this so we can assert the correct command</span>
<span class="c1">// is run.</span>
<span class="nc">Str</span><span class="o">::</span><span class="nf">createRandomStringsUsing</span><span class="p">(</span><span class="k">static</span> <span class="k">fn</span> <span class="p">():</span> <span class="kt">string</span> <span class="o">=></span> <span class="s1">'random'</span><span class="p">);</span>
<span class="p">}</span>
<span class="cd">/** @test */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">image_can_be_converted</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nc">Process</span><span class="o">::</span><span class="nf">fake</span><span class="p">([</span>
<span class="s1">'convert-image.sh tmp/random'</span> <span class="o">=></span> <span class="nc">Process</span><span class="o">::</span><span class="nf">result</span><span class="p">(</span>
<span class="n">output</span><span class="o">:</span> <span class="s1">'tmp/converted.webp'</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">]);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">post</span><span class="p">(</span><span class="nf">route</span><span class="p">(</span><span class="s1">'file.convert'</span><span class="p">),</span> <span class="p">[</span>
<span class="s1">'uploaded_file'</span> <span class="o">=></span> <span class="nc">UploadedFile</span><span class="o">::</span><span class="nf">fake</span><span class="p">()</span><span class="o">-></span><span class="nf">image</span><span class="p">(</span><span class="s1">'dummy.png'</span><span class="p">),</span>
<span class="p">])</span>
<span class="o">-></span><span class="nf">assertOk</span><span class="p">()</span>
<span class="o">-></span><span class="nf">assertExactJson</span><span class="p">([</span>
<span class="s1">'message'</span> <span class="o">=></span> <span class="s1">'File converted successfully!'</span><span class="p">,</span>
<span class="s1">'path'</span> <span class="o">=></span> <span class="s1">'tmp/converted.webp'</span><span class="p">,</span>
<span class="p">]);</span>
<span class="nc">Process</span><span class="o">::</span><span class="nf">assertRan</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="kt">PendingProcess</span> <span class="nv">$process</span><span class="p">):</span> <span class="kt">bool</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$process</span><span class="o">-></span><span class="n">command</span> <span class="o">===</span> <span class="s1">'convert-image.sh tmp/random'</span>
<span class="o">&&</span> <span class="nv">$process</span><span class="o">-></span><span class="n">path</span> <span class="o">===</span> <span class="nf">base_path</span><span class="p">()</span>
<span class="o">&&</span> <span class="nv">$process</span><span class="o">-></span><span class="n">timeout</span> <span class="o">===</span> <span class="mi">30</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="cd">/** @test */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">video_can_be_converted</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nc">Process</span><span class="o">::</span><span class="nf">fake</span><span class="p">([</span>
<span class="s1">'convert-video.sh tmp/random'</span> <span class="o">=></span> <span class="nc">Process</span><span class="o">::</span><span class="nf">result</span><span class="p">(</span>
<span class="n">output</span><span class="o">:</span> <span class="s1">'tmp/converted.mp4'</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">]);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">post</span><span class="p">(</span><span class="nf">route</span><span class="p">(</span><span class="s1">'file.convert'</span><span class="p">),</span> <span class="p">[</span>
<span class="s1">'uploaded_file'</span> <span class="o">=></span> <span class="nc">UploadedFile</span><span class="o">::</span><span class="nf">fake</span><span class="p">()</span><span class="o">-></span><span class="nf">create</span><span class="p">(</span><span class="s1">'dummy.mp4'</span><span class="p">),</span>
<span class="p">])</span>
<span class="o">-></span><span class="nf">assertOk</span><span class="p">()</span>
<span class="o">-></span><span class="nf">assertExactJson</span><span class="p">([</span>
<span class="s1">'message'</span> <span class="o">=></span> <span class="s1">'File converted successfully!'</span><span class="p">,</span>
<span class="s1">'path'</span> <span class="o">=></span> <span class="s1">'tmp/converted.mp4'</span><span class="p">,</span>
<span class="p">]);</span>
<span class="nc">Process</span><span class="o">::</span><span class="nf">assertRan</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="kt">PendingProcess</span> <span class="nv">$process</span><span class="p">):</span> <span class="kt">bool</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$process</span><span class="o">-></span><span class="n">command</span> <span class="o">===</span> <span class="s1">'convert-video.sh tmp/random'</span>
<span class="o">&&</span> <span class="nv">$process</span><span class="o">-></span><span class="n">path</span> <span class="o">===</span> <span class="nf">base_path</span><span class="p">()</span>
<span class="o">&&</span> <span class="nv">$process</span><span class="o">-></span><span class="n">timeout</span> <span class="o">===</span> <span class="mi">30</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="cd">/** @test */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">error_is_returned_if_the_file_cannot_be_converted</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nc">Process</span><span class="o">::</span><span class="nf">fake</span><span class="p">([</span>
<span class="s1">'convert-video.sh tmp/random'</span> <span class="o">=></span> <span class="nc">Process</span><span class="o">::</span><span class="nf">result</span><span class="p">(</span>
<span class="n">errorOutput</span><span class="o">:</span> <span class="s1">'Something went wrong!'</span><span class="p">,</span>
<span class="n">exitCode</span><span class="o">:</span> <span class="mi">1</span> <span class="c1">// Error exit code</span>
<span class="p">)</span>
<span class="p">]);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">post</span><span class="p">(</span><span class="nf">route</span><span class="p">(</span><span class="s1">'file.convert'</span><span class="p">),</span> <span class="p">[</span>
<span class="s1">'uploaded_file'</span> <span class="o">=></span> <span class="nc">UploadedFile</span><span class="o">::</span><span class="nf">fake</span><span class="p">()</span><span class="o">-></span><span class="nf">create</span><span class="p">(</span><span class="s1">'dummy.mp4'</span><span class="p">),</span>
<span class="p">])</span>
<span class="o">-></span><span class="nf">assertStatus</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span>
<span class="o">-></span><span class="nf">assertExactJson</span><span class="p">([</span>
<span class="s1">'message'</span> <span class="o">=></span> <span class="s1">'Something went wrong!'</span><span class="p">,</span>
<span class="p">]);</span>
<span class="nc">Process</span><span class="o">::</span><span class="nf">assertRan</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="kt">PendingProcess</span> <span class="nv">$process</span><span class="p">):</span> <span class="kt">bool</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$process</span><span class="o">-></span><span class="n">command</span> <span class="o">===</span> <span class="s1">'convert-video.sh tmp/random'</span>
<span class="o">&&</span> <span class="nv">$process</span><span class="o">-></span><span class="n">path</span> <span class="o">===</span> <span class="nf">base_path</span><span class="p">()</span>
<span class="o">&&</span> <span class="nv">$process</span><span class="o">-></span><span class="n">timeout</span> <span class="o">===</span> <span class="mi">30</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>The above tests will ensure the following:</p>
<ul>
<li>If a file is an image, only the <code>convert-image.sh</code> script is run.</li>
<li>If a file is a video, only the <code>convert-video.sh</code> script is run.</li>
<li>If the conversion script fails, an error is returned.</li>
</ul>
<p>You may have noticed that we've used the <code>preventStrayProcesses</code> method. This ensures that only the commands we have specified are run. If any other commands are run, the test will fail. This is handy for giving you confidence that the scripts aren't accidentally running any external processes on your system.</p>
<p>We've also explicitly tested that the commands are run with the expected path and timeout. This is to ensure that the commands are run with the correct settings.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Hopefully, this article has shown you how to create and test your own Artisan commands. It should have also shown you how to run OS processes from your Laravel application and test them. You should now be able to implement both of these features in your projects to add extra functionality to your applications.</p>