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.
git initgit add .git commit -m "Initial Laravel Breeze and DB Models"git branch -M maingit remote add origin https://github.com/[your_name]/[your_repo].gitgit 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.
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.
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:
#123
(auto-incremental for that repository) which you can then reference in the descriptions of git commits and PRsdev
referencing that number. For example, branch name can be "123-links-crud". That may help to quickly identify what each developer is working on.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.
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>
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.
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.
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.