megacolorboy

Abdush Shakoor's Weblog

Writings, experiments & ideas.

Clean Controllers in Laravel: Best practices for handling form submissions

Learn how to write clean Laravel controllers for efficient contact form handling using validation, Eloquent, Mail, and events.

Unlike most frameworks, Laravel is an unopinionated framework and doesn't force the developer to follow a particular pattern.

In this article, I'm going to show you how your codebase could benefit by following Laravel's best practices like separation of concerns, utilizing Laravel's in-built features like Eloquent models, Form Requests and Mail.

To begin with, here's a dirty method that handles a simple contact form submission:

<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

class ContactController extends Controller
{
    public function submit(Request $request)
    {
        // Validate the form inputs
        $request->validate([
            'name' => 'required',
            'email' => 'required|email',
            'message' => 'required',
        ]);

        // Get the form data
        $name = $request->input('name');
        $email = $request->input('email');
        $message = $request->input('message');

        // Save the form data in the database
        DB::table('contact_forms')->insert([
            'name' => $name,
            'email' => $email,
            'message' => $message,
        ]);

        // Send an email to the user
        $mailData = [
            'name' => $name,
            'email' => $email,
            'message' => $message,
        ];

        Mail::send('emails.contact', $mailData, function ($message) use ($email) {
            $message->to($email)->subject('Thank you for contacting us');
        });

        // Return a response to the user
        return redirect()->back()->with('success', 'Thank you for contacting us! We will get back to you soon.');
    }
}

To most developers, this method might seem straight-forward and they might even debate with you that it's completely fine. But allow me to highlight the issues of that this method brings to the table:

  1. Lack of separation of concerns: The controller is responsible for handling form validation, saving data to the database, and sending an email. This violates the Single Responsibility Principle.

  2. Direct usage of the database query builder: Instead of utilizing Eloquent models for database interactions, the example directly uses the query builder, which can make the code less maintainable and harder to read.

  3. Inline email sending: The email sending functionality is implemented inline within the controller method, making the code harder to test and reuse.

  4. Lack of error handling: The example does not handle any potential errors that may occur during form validation, database insertion, or email sending.

Now, it's time to make it cleaner.

Save data using Form Requests and Laravel's Eloquent ORM

Let's start with creating a form request that is dedicated to handling the validation logic:

  1. Quickly, open up your terminal and type the following command:
php artisan make:request ContactFormRequest

This command will generate a new file named ContactFormRequest.php in the app/Http/Requests directory.

  1. Open the ContactFormRequest.php file and modify it as follows:
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ContactFormRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // Set the authorization logic based on your requirements
        // For example, you may check if the user is authenticated or has certain permissions.
        // Return true if the user is authorized to submit the form; otherwise, return false.
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required',
            'email' => 'required|email',
            'message' => 'required',
        ];
    }

    public function messages()
    {
        return [
            'name.required' => 'Please enter your name',
            'email.required' => 'Please enter your email address',
            'email.email' => 'Please enter a valid email address',
            'message.required' => 'Write a message before submit the form',
        ];
    }
}

In the rules() method, you can define validation rules for each field in the form. You can adjust according to your own preferences and if you want to know more about them, read this documentation.

  1. Save the ContactFormRequest.php file.

Great, now you have a separate class for validating your contact form and this promotes code-reusability, improves code readability and separates the validation logic from your controller.

Now let's update the submit() method in the ContactController.php example:

<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Http\Requests\ContactFormRequest;
use App\Models\ContactFormModel;

class ContactController extends Controller
{
    public function submit(ContactFormRequest $request)
    {
        // Validate the form inputs
        // All of the validated form data is accessible via this property.
        $validated = $request->validated();

        // Save the submission
        ContactFormModel::create($validated);

        // Send an email to the user
        $mailData = [
            'name' => $name,
            'email' => $email,
            'message' => $message,
        ];

        Mail::send('emails.contact', $mailData, function ($message) use ($email) {
            $message->to($email)->subject('Thank you for contacting us');
        });

        // Return a response to the user
        return redirect()->back()->with('success', 'Thank you for contacting us! We will get back to you soon.');
    }
}

Ensure that you have an appropriate model for your contact_forms table like ContactFormModel.php and update the following like so:

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ContactForm extends Model
{
    protected $table = 'contact_forms';

    // Define any additional model configurations, relationships, or methods.
    protected $fillable = [
        'name',
        'email',
        'message'
    ];

    protected $timestamps = true;
}

This allows you to leverage Laravel's ORM in which you could benefit from easy quering, relationship management and automatic timestamps.

Making use of Laravel Events and Mail

  1. Generate a new event class by writing the following artisan command:
php artisan make:event ContactFormSubmitted

This will generate a ContactFormSubmitted class in the app/Events directory.

  1. Open up ContactFormSubmitted.php file and update as follows:
<?php
namespace App\Events;

use App\Models\ContactForm;
use Illuminate\Foundation\Events\Dispatchable;

class ContactFormSubmitted
{
    use Dispatchable;

    public $contactForm;

    /**
     * Create a new event instance.
     *
     * @param  ContactForm  $contactForm
     * @return void
     */
    public function __construct(ContactForm $contactForm)
    {
        $this->contactForm = $contactForm;
    }
}
  1. Next, let's generate an event listener using artisan command:
php artisan make:listener SendContactFormEmail --event=ContactFormSubmitted

This command will generate SendContactFormEmail event listener in the app/Listeners directory.

  1. Open the SendContactFormEmail.php file and update as follows:
<?php
namespace App\Listeners;

use App\Events\ContactFormSubmitted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail;
use App\Mail\ContactFormSubmitted as ContactFormSubmittedMail;

class SendContactFormEmail implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * Handle the event.
     *
     * @param  ContactFormSubmitted  $event
     * @return void
     */
    public function handle(ContactFormSubmitted $event)
    {
        $contactForm = $event->contactForm;

        // Send an email to the user
        Mail::to($contactForm->email)
            ->send(new ContactFormSubmittedMail($contactForm));
    }
}
  1. Create a mailable class using the following artisan command:
php artisan make:mail ContactFormSubmitted

This command will generate a new ContactFormSubmitted mailable class in the app/Mail directory.

Now, update the ContactFormSubmitted.php file as follows:

<?php
namespace App\Mail;

use App\Models\ContactForm;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ContactFormSubmitted extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public $contactForm;

    /**
     * Create a new message instance.
     *
     * @param  ContactForm  $contactForm
     * @return void
     */
    public function __construct(ContactForm $contactForm)
    {
        $this->contactForm = $contactForm;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->subject('Thank you for contacting us')
            ->view('emails.contact')
            ->with('contactForm', $this->contactForm);
    }
}
  1. Let's open up the ContactController.php and import the necessary classes on top:
<?php
use App\Events\ContactFormSubmitted;
use Illuminate\Support\Facades\Mail;
  1. Lastly, update the submit() method as follows:
<?php
use App\Http\Requests\ContactFormRequest;
use App\Models\ContactForm;
use App\Events\ContactFormSubmitted;
use Illuminate\Support\Facades\Mail;
use App\Mail\ContactFormSubmitted;

class ContactController extends Controller
{
    public function submit(ContactFormRequest $request)
    {
        // Validated form data is accessible via the $validatedData property of the form request instance
        $validatedData = $request->validated();

        // Save the form data in the database using Eloquent
        $contactForm = new ContactForm();
        $contactForm->name = $validatedData['name'];
        $contactForm->email = $validatedData['email'];
        $contactForm->message = $validatedData['message'];
        $contactForm->save();

        // Dispatch an event to handle email sending asynchronously
        event(new ContactFormSubmitted($contactForm));

        // Return a response to the user
        return redirect()->back()->with('success', 'Thank you for contacting us! We will get back to you soon.');
    }
}

Ah! Finally, this looks much better now, doesn't it?

By making use of Laravel's Mail method, events and listeners, we've separated the logic for sending emails from the controller. The email is now sent asynchronously using an event and listener combination. This approach improves code organization, maintainability, and scalability.

Hope you found this article to be useful.

Thoughts about ChatGPT and the impact on software developers

Will AI-powered tools replace developers in the near future?

This is something I wanted to address for quite sometime but never got the time to talk about my opinions/views about it.

I have been reading many articles and watching a few videos on YouTube in which the authors often contemplate about the future of developers and whether their jobs might be replaced.

If you think that's the case, I'd like to share one of my favorite quotes that might change your mindset:

Software is just a tool to help accomplish something for people – many programmers never understood that. Keep your eyes on the delivered value, and don’t over-focus on the specifics of the tools. — John Carmack

Ever since, ChatGPT went viral, I (and some of my team members) thought of incorporating it to help educate myself during development.

Will it ever replace humans?

Just like the quote above, software is just a mere tool that helps accomplish your end goals. It definitely isn't going to replace a developer with years of experience, let alone, a human being but rather, as a tool or learning aid to help accelerate our ways of being productive.

There are tools like GitHub Copilot (powered by ChatGPT) that helps developers in troubleshooting, refactoring and solving well-defined problems but however, I feel it still has a long way to be able to solve really complex problems in which otherwise could be solved by a software developer.

I'd say that it's crucially important for you to know and be aware of the capabilities and potential of AI-based tools but not to worry too much about it's impact in the industry.

In my opinion, I'd say that a developer who knows how to be effective using AI-powered tools as a learning aid are more likely to replace an developer who aren't aware of them at all.

Here's another quote from John Carmack:

If you build full "product skills" and use the best tools for the job, which today might be hand coding, but later maybe AI guiding, you will be probably be fine. — John Carmack

It's important to view yourself as a problem solver who uses technology rather than someone who simply "codes" for a living and would get replaced in the near future.

What many people don't realize is that a software developer isn't someone who only writes code but also someone who solves complex problems and makes critical decisions. From my experience, I can tell you that it takes a lot of decisions to go from design to deployment and that involves a team that's made up of various skillsets that includes project management, design, development, QA and deployment.

Currently, AI might be good at solving well-defined or common problems but it can never replace the decision-making needed for a successful project.

Can we trust it?

Not yet. ChatGPT ain't AGI thus it has limitations such as that it "hallucinates" i.e. comes up with random facts or solutions that aren't even logical to the problem defined. Whereas, if you look for the problem on StackOverflow, you're more likely to copy/paste a solution that's already recommended by the community.

Exploring ChatGPT

As I had mentioned above, it's quite good at solving well-defined problems and here are a few examples that has helped saved me time:

  • Refactoring methods and repetitive code
  • Writing a method that validates email using regular expressions
  • Writing a simple image gallery component using ReactJS
  • Helped me learn new languages and concepts faster

Oh yes, it did make plenty of mistakes but with all that said, I think it's a great time to learn something new.

Conclusion

My take is that to not overestimate the progression of AI in the near future and underestimate it's impact in the distant future.

Hope you liked reading this article.

List tables that don't have primary keys in a MySQL database

Whenever I restore a database dump on a MySQL InnoDB Cluster, most of the time, the error I face is related to missing PRIMARY KEY attribute because as per MySQL's official documentation:

Every table that is to be replicated by the group must have a defined primary key, or primary key equivalent where the equivalent is non-null unique key.

What I would usually do is, run a simple query to determine the list of the tables that don't the PRIMARY KEY attribute:

select tab.table_schema as database_name,
       tab.table_name
from information_schema.tables tab
left join information_schema.table_constraints tco
          on tab.table_schema = tco.table_schema
          and tab.table_name = tco.table_name
          and tco.constraint_type = 'PRIMARY KEY'
where tco.constraint_type is null
      and tab.table_schema not in('mysql', 'information_schema', 'performance_schema', 'sys')
      and tab.table_type = 'BASE TABLE'
      and tab.table_schema = 'your_database_name_here'
order by tab.table_schema,
         tab.table_name;

Hope you found this tip useful!

5th February 2023 — Blogmarks

Playing around with ImageMagick

ImageMagick is not a new tool for me but I haven't really talked about how it comes in handy whenever I wanted to resize and/or compress images efficiently using the terminal instead of using some image conversion service found for free online.

To get started, make sure you install ImageMagick on your machine. This is the command I use for Fedora Linux:

sudo dnf install ImageMagick

For example, if you wanted to compress an image's quality to 80%, here's you can do it:

convert -quality 80% src.png dest.png

Great! What if you want to do it for hundreds of images in a directory, then you can easily do something like this:

find . -type f -name '*.png' -exec bash -c 'convert -quality 80% "$1" "${1%}_compressed.png" ' bash  {} \;

You can do the same to resize the images while keeping original aspect ratio intact:

convert -resize 450x450 src.png dest.png

And similar to the one above, you can do the same for multiple images like so:

find . -type f -name '*.png' -exec bash -c 'convert -resize 450x450 "$1" "${1%}_resized.png" ' bash  {} \;

I have just scraped the surface of this image manipulation tool and if you are interested to know more, read the official manpage on how to use it.

Hope you found this tip useful!

Install Cisco Anyconnect Client on Fedora Linux

Cisco Anyconnect Client is a SSL VPN Client with VPN functionalites that enables an organization to secure its endpoints. Although, I use it a lot at office, I have only used it on Windows 10 so far.

One day, when I was on my vacation, I had to connect to a server remotely but this time, I tried doing it via Linux and it worked well!

Download Cisco AnyConnect Client for Linux

Fortunately, the client is available for Windows, macOS and Linux. You can go to the Downloads page and download the tarball archive for Linux.

After downloading, you can extract the tarball file:

tar -xvf anyconnect-linux64-4.10.00093-predeploy-k9.tar.gz

Install Cisco AnyConnect Client for Linux

Now that the file has been extracted, you can navigate to the vpn directory in anyconnect-linux64-* directory:

cd anyconnect-linux64-*/vpn/

Execute the vpn_install.sh script:

sudo ./vpn_install.sh

Follow the instruction steps and you're ready to use it.

Connect to VPN via Terminal

The GUI version was crashing on both KDE and GNOME desktop environments. Whereas, it was a smooth experience when connecting via the Terminal.

For easy access, you can create an alias in your .bashrc file:

alias anyconnectvpn="/opt/cisco/anyconnect/bin/vpn"

Save and restart your terminal for the changes to take effect.

After that, you can connect to VPN like this:

anyconnectvpn -s connect <IP_ADDRESS>

Once executed, you'll be prompted to enter your credentials and if all goes fine, you should be connected.

Disconnect VPN via the Terminal

You can execute the following command to disconnect:

anyconnectvpn -s disconnect <IP_ADDRESS>

Configuring your VPN connection

Sometimes, it can be tedious to write these commands all the time especially when you're in a rush, this can really get in your way.

To solve that, you can easily store your credentials in a file like .my_vpn_creds in your home directory:

y
<username>
<password>

Save the file and create another file named my_vpn_connect.sh:

#!/bin/bash

VPN_SERVER="<IP_ADDRESS>"

echo "Connecting to VPN.."
/opt/cisco/anyconnect/bin/vpn -s  < ~/.my_vpn_creds connect ${VPN_SERVER}

Save the file and make the file executable:

sudo chmod +x my_vpn_connect.sh

Now, you'll be connected to the VPN automatically by just executing this script:

./my_vpn_connect.sh

This saved me a lot of time and I hope it does the same for you as well! πŸ€—

Hope you found this article useful!

Setup a virtual environment for Python development

virtualenv is a tool used that allows you to install and isolate Python packages that are specific to your project rather than installing them globally on your system, which might, at some point, break other tools or services in your system.

In this simple guide, I'll show you how to install and create your own Python virtual environment for development.

Before you begin, please make sure that you have installed Python 3 or greater on your system.

Install virtualenv

Using the pip tool, you can execute the following command:

sudo pip install virtualenvwrapper

Then you need to add the following lines to your .bashrc startup file (found in the home directory).

These lines determine where your virtual environments should live, the location of the script installed and the location of the development project directories:

export WORKON_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
export VIRTUALENVWRAPPER_VIRTUALENV_ARGS=' -p /usr/bin/python3 '
export PROJECT_HOME=$HOME/Devel
source /usr/local/bin/virtualenvwrapper.sh

Next, restart the startup file:

source .bashrc

Once done, it'l execute a bunch of scripts for a few minutes and once the installation is successful, you can start using the mkvirtualenv command to create your virtual environment.

Creating a virtual environment

Really, it's quite trivial to create a virtual environment like this:

mkvirtualenv my_virtual_environment

Once done, you'll notice that your terminal looks something like this:

(my_virtual_environment) user@desktop:~$

This indicates that you are inside the virtual environment and can begin installing your Python packages and begin developing your application.

Using the virtual environment

There are a few commands that I'd like to share (and you should also know) while working with a virtual environment:

  • deactivate — get out of the current virtual environment
  • workon — list available virtual environments on the system
  • workon name_of_environment — activate the specified virtual environment
  • rmvirtualenv name_of_environment — remove the specified virtual environment

Conclusion

If you are a Python developer or write code in Python, then virtual environments are super handy and disposable especially while testing and working on different projects.

Unless, your project is package-dependant, you don't really need to isolate your development environment.

Hope you found this article useful!

Install TeamViewer on Fedora Linux 36/37

If you are using Fedora Linux or any other Linux distribution and want a short guide on how to install TeamViewer on your workstation, then this article is for you.

Usually, I use it access remote servers or my computer at work but I myself never did it on a Linux workstation. It worked out for me, so it should work for you too!

1. Download TeamViewer RPM Package

You can find the latest release on TeamViewer's official site.

Make sure that you have install wget on your system to easily download it via the Terminal:

wget https://download.teamviewer.com/download/linux/teamviewer.x86_64.rpm

2. Download minizip module

During installation, it failed for me as I was missing a certain libminizip.so module. So, I looked up online and found the module and downloaded it on my workstation:

wget https://rpmfind.net/linux/fedora/linux/updates/36/Everything/x86_64/Packages/m/minizip-compat-1.2.11-33.fc36.x86_64.rpm

Once the download is complete, you can install it:

rpm -i minizip-compat-1.2.11-33.fc36.x86_64.rpm

3. Install TeamViewer

Now, the final step is to install the RPM package for TeamViewer:

rpm -i teamviewer_15.37.3.x86_64.rpm

Hope you found this short guide useful!