Back to Course |
[NEW] Laravel Project: From Start to Finish

GitHub: Branches, Issues and First CRUD

Next, we move on creating a first CRUD of Links.

This is the final visual result, after this lesson:

It will be a simple list/create/edit/delete feature, so not that much to talk about from code perspectice. As with everything in this course, let's focus on processes: the important part is that we will set up GitHub and branches.

Let's configure and prepare all on GitHub BEFORE writing any new code.

  1. Create an empty GitHub repository
  2. Follow its instructions to push the code so far
git init
git add .
git commit -m "Initial Laravel Breeze and DB Models"
git branch -M main
git remote add origin https://github.com/[your_name]/[your_repo].git
git push -u origin main

The default first branch is called main (unless you changed it to something else, but I will go with the defaults).

And now the question is whether we should continue working with that branch, or create a separate one.

In this course, we assume you're working in a team of at least two people, so you do need to collaborate on multiple features at once, before pushing them to live.

With that in mind, it's crucial to push changes to main only when your intention is to deploy them to the live server.

All other time you should be working on a different branch.

First, you need to create a develop (or we called it dev) branch from main.

git checkout -b dev

Currently, your dev and main branches are in sync, with identical code versions.

But the idea here is that the main branch will be attached to the live server, and the code in the dev branch will by used for testing the latest "in progress" version of the project, on so-called staging server.

In other words, dev branch will always be ahead of main with some new code, features and fixes:

Only when you're ready to push them live, you will make a Pull Request from dev to main and merge those changes, to later (or immediately) be deployed.


Only "dev" VS Branches for Features

Now, do we write our code and push it to the dev branch? Not necessarily. There are two strategies possible:

1. No feature branches (not recommended): you work on the dev branch together with other devs but work on separate things: different features and files in the code.

It may be convenient, but more risky: requires every developer to git pull the latest changes from dev branch before starting their work session, to avoid/minimize conflicts.

With that, there's an "unwritten rule" that you should push to dev branch only the code that is pretty much finished, with fully completed feature/task. You can't push "work in progress" because other developers would be confused when they pull down the changes.

It's easier to follow in small projects with not many code changes and almost no conflicts. Also may work if you're working solo.

2. Feature branches (recommended): before starting the work on any specific feature, developer creates a "feature branch" from dev with the name describing that feature, like "feature-links-crud". Then they work on that branch individually, without looking at other branches or caring what other devs work on.

Until they need to push their changes, they submit a Pull Request from their feature branch to dev and only then they see if there are any conflicts needed to be resolved.

This approach of feature branches is not only more calm for developers in day-to-day work, but it also allows to clearly see which code belongs to which feature. It becomes very important later, if something goes wrong and we need to quickly track down which code/branch had introduced a bug and when.

In this lesson for the Links CRUD, I will deliberately NOT use the feature branch, to show you the first scenario. We will switch to creating feature branches in the next lessons.


GitHub Issues

I just mentioned the benefit of tracking down the code by feature. But even a better way is to attach those features to a task in a project management software?

Over the years, our team tried many tools: Trello, Asana, Monday, you name it. But to have the order from technical side, nothing has beaten the internal GitHub feature of GitHub Issues. It just feels native. The process is this:

  • You create a GitHub issue and (optionally) assign a teammate
  • That issue automatically gets an ID number from GitHub, like #123 (auto-incremental for that repository) which you can then reference in the descriptions of git commits and PRs
  • That teammate creates a feature branch from dev referencing that number. For example, branch name can be "123-links-crud". That may help to quickly identify what each developer is working on.
  • When the feature is done, the commit message should contain the issue number with a hashtag sign and "magic" words like "resolves". Example of a commit message: "Links CRUD - resolves #123".

If you do all of that, then GitHub will automatically interlink everything: on the web, you would be able to quickly navigate between the issue, the pull request and the specific commit.

Let me demonstrate it all in our example.

We create a GitHub issue:

GitHub auto-assigns an ID of #1 to it. We will use that ID later when pushing the code with solutions.

Then we switch to dev branch:

git checkout dev

In reality, from here you shouldn't work with main branch directly at all. In many companies, that branch is even protected from ever pushing code straight to it. That branch is "sacred" only for Pull Requests into it.

Next, we code our feature.


Code of Links CRUD: Quick Overview

This course is not that much about coding and more about processes, so I will just briefly summarize what we're doing here.

First, we have a CRUD Controller:

app/Http/Controllers/LinkController.php

class LinkController extends Controller
{
public function index()
{
$links = Link::query()
// Filter by tenant id (user_id)
->where('user_id', auth()->id())
->orderBy('id', 'desc')
->paginate(50);
 
return view('links.index', [
'links' => $links,
]);
}
 
public function create()
{
$users = User::all();
 
return view('links.create', [
'users' => $users,
]);
}
 
public function store(StoreLinkRequest $request)
{
$link = Link::create(
$request->validated() + [
'user_id' => auth()->id(),
]
);
 
// If there is no position, set it to the last
if (!$link->position) {
$link->position = Link::max('position') + 1;
$link->save();
}
 
return redirect()->route('links.index')
->with('message', 'Link created successfully.');
}
 
public function edit(Link $link)
{
// Check if user is the owner of the link
abort_unless($link->user_id === auth()->id(), 404);
 
$users = User::all();
 
return view('links.edit', [
'link' => $link,
'users' => $users,
]);
}
 
public function update(UpdateLinkRequest $request, Link $link)
{
abort_unless($link->user_id === auth()->id(), 404);
 
$link->update($request->validated());
 
return redirect()->route('links.index')
->with('message', 'Link updated successfully.');
}
 
public function destroy(Link $link)
{
abort_unless($link->user_id === auth()->id(), 404);
 
$link->delete();
 
return redirect()->route('links.index')
->with('message', 'Link deleted successfully.');
}
}

Then we have a simple index table:

resources/views/links/index.blade.php

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Links List') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
@if (session('message'))
<div class="mb-4 font-medium text-sm text-green-600">
{{ session('message') }}
</div>
@endif
 
<div class="mt-4">
<a href="{{ route('links.create') }}"
class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Add New
Link</a>
</div>
 
<table class="table-auto w-full mt-4 border">
<thead>
<tr>
<th class="px-4 py-2">Title</th>
<th class="px-4 py-2">URL</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($links as $link)
<tr>
<td class="border px-4 py-2">{{ $link->title }}</td>
<td class="border px-4 py-2">
<a href="{{ $link->url }}" target="_blank"
class="text-blue-400 underline underline-offset-3 decoration-blue-200">{{ $link->url }}</a>
</td>
<td class="border px-4 py-2">
<a href="{{ route('links.edit', $link->id) }}"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2.5 px-4 rounded">Edit</a>
<form action="{{ route('links.destroy', $link->id) }}" method="POST" class="inline-block">
@csrf
@method('DELETE')
<button type="submit"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
Delete
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
 
<div class="mt-4">
{{ $links->links() }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

And the form for creating a new link:

Note: Edit form is identical, so I'm not showing it here.

resources/views/links/create.blade.php

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Create Link') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
 
<form method="POST" action="{{ route('links.store') }}">
@csrf
 
<div class="mb-4">
<label for="title" class="block text-sm font-medium text-gray-700">Title</label>
<input type="text" name="title" id="title"
class="form-input rounded-md shadow-sm mt-1 block w-full" value="{{ old('title') }}"
autofocus/>
@error('title')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
 
<div class="mb-4">
<label for="url" class="block text-sm font-medium text-gray-700">URL</label>
<input type="url" name="url" id="url"
class="form-input rounded-md shadow-sm mt-1 block w-full" value="{{ old('url') }}"/>
@error('url')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
 
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description" id="description"
class="form-textarea rounded-md shadow-sm mt-1 block w-full">{{ old('description') }}</textarea>
@error('description')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
 
<div class="mb-4">
<label for="position" class="block text-sm font-medium text-gray-700">Position</label>
<input type="text" name="position" id="position"
class="form-input rounded-md shadow-sm mt-1 block w-full"
value="{{ old('position') }}"/>
@error('position')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
 
<div class="mb-4">
<button type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Create
</button>
</div>
 
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

Pushing to Dev Branch

Next, we push the code to dev, referencing the issue number of #1:

git add .
git commit -m "Links CRUD - resolves #1"
git push origin dev

Notice: of course, you may push multiple commits for the same feature, not always you get it from the first time. If you work on dev branch, just reference the same issue ID in every commit, then. With feature branches, it is solved another way, we'll get to that in the next lesson.

And now, as a result we have everything interlinked.

  1. We have the task in the list of Issues

  1. We can navigate from the Issue to the specific Commit that resolves it

  1. And vice versa: if you're looking at the code of commit, there's a link to the original Issue:

It not only helps to review the code/issue at the moment, but also helps to understand what happened and when and why, in the past. Both from the task perspective and what code is related to that specific feature. Your teammates and future self will thank you for that.

So yeah, we've completed the feature of Links CRUD. Or... not yet. The final step is to write automated tests for it, and we will introduce a feature branch with it, in the next lesson.


P.S. GitHub is For Devs: What About Clients?

Final "side note" for this lesson: if you work with non-technical project managers and product owners, they all need to learn GitHub and how to create the GitHub Issue? Or how do they see the progress?

No, not necessarily. There are various ways to handle it, but we usually used two separate systems: client-facing project management software, and then we transformed only the confirmed tasks to GitHub issues.

It doesn't only filters the REAL tasks from the general "in discussion" pile, but also most clients do prefer to see the tasks in "their language", without technical details. And yes, it requires to update data in two systems, but in our experience, it's not that much extra work, if done properly.

Also, many project management tools like Jira/Trello/etc have integrations with GitHub and allow to conveniently navigate between them.

But, as everything, it depends on your/client team structure and preferences.