This is a mock SaaS app built in the Laravel framework that utilises Laravel's factories and seeders to build a DB, then utilises models, controllers, routes and resource controllers to access, display and edit that DB. The app is styled with Tailwind and has some interactivity written in the Alpine.JS framework.
Find the hosted project Here
Find the GitHub link Here
/routes/web.php
// Login / Logout:
Route::post('login', [LoginController::class, 'store']);
Route::get('login', [LoginController::class, 'index']);
Route::get('logout', [LoginController::class, 'destroy']);
/app/Http/Controllers/LoginController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller {
public function index()
{
return view('dashboard');
}
public function store(Request $request)
{
//validate the request
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
// attempt to log in the user based on the provided credentials
if (Auth::attempt($credentials)) {
// 'session regenerate' in place to stop session fixation attacks.
request()->session()->regenerate();
// return to the home page with the success message
return redirect('/')->with('success', 'Welcome Back!');
} else {
//auth failed, flash error message
throw ValidationException::withMessages([
'email' => 'Your provided credentials are invalid. Please try again',
]);
}
}
public function destroy(Request $request)
{
auth()->logout();
$request->session()->invalidate();
return redirect('/login')->with('success', 'Goodbye!');
}
<body class=" m-0 w-screen grow-div">
<x-header></x-header>
<main class="flex justify-center items-center bg-gray-500 m-0 grow w-screen">
@guest
<x-login />
@else
{{ $slot }}
@endguest
</main>
<x-flash />
</body>
CompanyFactory.php
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class CompanyFactory extends Factory
{
private static $id = 0;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
// array of logos image file names to seed the database with.
$logos = ['company-logo-1.png', 'company-logo-2.png',
'company-logo-3.png', 'company-logo-4.png', 'company-logo-5.png',
'company-logo-6.png', 'company-logo-7.png', 'company-logo-8.png',
'company-logo-9.png', 'company-logo-10.png'
];
// $path was created to change the pathing for the img url if necessary.
$path = '/storage/img/';
// alternative method to create a path to a random logos
// which doesn't require an array: array method chosen due to
// flexibility of names: 'logos' => $path . 'companies-logos-' .
// $this->faker->unique()->randomDigit . '.png',
return [
'logos' => $path . $logos[self::$id++],
'name' => $this->faker->company,
'email'=> $this->faker->email,
'website'=> $this->faker->domainName
];
}
}
EmployeeFactory.php
<?php
namespace Database\Factories;
use App\Models\Company;
use Illuminate\Database\Eloquent\Factories\Factory;
class EmployeeFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
// There are 10 companies, so employees
// can be associated with a random digit for their foreign key
'company_id' => rand(1, 10),
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email,
'phone_number' => $this->faker->phoneNumber,
];
}
}
LogoFactory.php
<?php
namespace Database\Factories;
use App\Models\Company;
use Illuminate\Database\Eloquent\Factories\Factory;
class LogoFactory extends Factory
{
// private static $company_id = 1;
private static $logo_index = 0;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
// array of logos image file names to seed the database with.
$logos = ['company-logo-1.png', 'company-logo-2.png',
'company-logo-3.png', 'company-logo-4.png', 'company-logo-5.png',
'company-logo-6.png', 'company-logo-7.png', 'company-logo-8.png',
'company-logo-9.png', 'company-logo-10.png'
];
// $path was created to change the pathing for the img url if necessary.
$path = '/storage/img/';
return [
'file_path' => $path . $logos[(self::$logo_index++)],
];
}
}
DatabaseSeeder.php
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Company;
use App\Models\Logo;
use App\Models\Employee;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
User::create([
'name' => 'Admin',
'email' => 'admin@admin.com',
'password' => bcrypt('uxiQxfG8YK5W2sG')
]);
Company::factory(10)->create();
Logo::factory(10)->create();
Employee::factory(100)->create();
}
}
app/Models/Company.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Kyslik\ColumnSortable\Sortable;
class Company extends Model
{
use HasFactory;
use Sortable;
protected $fillable = [
'name',
'email',
'logos',
'website',
];
public $sortable = [
'name',
'email',
'website',
];
public function employees()
{
return $this->hasMany(Employee::class);
}
public function scopeFilter($query, array $filters)
{
$query->
when($filters['search'] ?? false, function ($query, $search) {
$query->where('name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%')
->orWhere('website', 'like', '%' . $search . '%');
});
}
}
app/Models/Employee.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Kyslik\ColumnSortable\Sortable;
class Employee extends Model
{
use HasFactory;
use Sortable;
protected $fillable = [
'company_id',
'first_name',
'last_name',
'email',
'phone_number',
];
protected $sortable = [
'company_id',
'first_name',
'last_name',
'email'
];
public function company()
{
return $this->belongsTo(Company::class);
}
public function scopeFilter($query, array $filters)
{
$query->when($filters['search'] ?? false, function ($query, $search) {
$query->where('first_name', 'like', '%' . $search . '%')
->orWhere('last_name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%')
->orWhereHas('company', function ($query) use ($search) {
$query->where('name', 'like', '%' . $search . '%');
});
});
}
}
app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
app/Models/Logo.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Logo extends Model
{
use HasFactory;
protected $fillable = [
'file_path'
];
}
app/Http/Controllers/EmployeeController.php
<?php
namespace App\Http\Controllers;
use App\Models\Employee;
use App\Models\Company;
use Illuminate\Validation\Rule;
class EmployeeController extends Controller
{
public function index()
{
return view('employees.index', [
'employees' => Employee::sortable()
->with('company')
->orderBy('first_name')
->orderBy('last_name')
->filter(request(['search']))
->paginate(10)
]);
}
public function create()
{
$employee = null;
return view('employees.create', [
'companies' => Company::all(),
]);
}
public function store()
{
$employee = Employee::create($this->validateEmployee());
return redirect()
->route('employees.show', $employee)
->with('success', 'Employee ' . $employee->first_name . ' ' . $employee->last_name . ' created!');
}
public function show(Employee $employee)
{
return view('employees.show', [
'employee' => Employee::find($employee->id),
'companies' => Company::all()
]);
}
public function edit(Employee $employee)
{
//Route model binding means we don't need to use findorfail() here
return view('employees.edit', [
'employee' => Employee::find($employee->id),
'companies' => Company::all()
]);
}
public function update(Employee $employee)
{
$attributes = $this->validateEmployee($employee);
$employee->update($attributes);
return redirect()->route('employees.show', [
'employee' => $employee,
])->with('success', $employee->first_name . ' ' . $employee->last_name . ' Updated!');
}
public function updateCompanyEmployee(Employee $employee, $company)
{
$attributes = $this->validateEmployee($employee);
$employee->update($attributes);
return redirect()->route()('/companies/' . $company->id, [
'company' => $company,
'employees' => Employee::where('company_id', $company->id)
->filter(request(['search']))
->sortable()
->orderBy('first_name')
->orderBy('last_name')
->paginate(10),
])->with('success', $employee->first_name . ' ' . $employee->last_name . ' Updated!');
}
public function destroy(Employee $employee){
$employee->delete();
return redirect()->route('employees.index')
->with('success', $employee->first_name . ' ' . $employee->last_name . ' Deleted!');
}
public function validateEmployee(?Employee $employee = null): array
{
$employee ??= new Employee();
return request()->validate([
'company_id' => 'required',
'first_name' => 'required|max:255',
'last_name' => 'required|max:255',
'email' => ['required', 'max:255',
'regex:/^([A-z\d\.-]+)@([A-z\d-]+)\.([A-z]{2,8})(\.[A-z]{2,8})?$/',
Rule::unique('employees', 'email')->ignore($employee)
],
'phone_number' => ['required', 'regex:/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/',
Rule::unique('employees', 'phone_number')->ignore($employee)]
]);
}
}
/app/Http/Controllers/CompanyController.php
<?php
namespace App\Http\Controllers;
use App\Models\Company;
use App\Models\Employee;
use App\Models\Logo;
use Illuminate\Validation\Rule;
use Illuminate\Http\Request;
class CompanyController extends Controller
{
public function index()
{
$companies = Company::filter(request(['search']))
->sortable()
->paginate(10);
return view('companies.index', compact('companies'));
}
public function create()
{
return view('companies.create', [
'companies' => Company::all(),
'logos' => Logo::all()
// 'image' => request()->file('image')->store('image', 'public')
]);
}
public function store(){
$company = Company::create($this->validateCompany());
return redirect()
->route('companies.index')
->with('success', $company->name . ' added!');
}
public function show(Company $company)
{
return view('companies.show', [
'company' => $company,
'employees' => Employee::where('company_id', $company->id)
->filter(request(['search']))
->sortable()
->orderBy('first_name')
->orderBy('last_name')
->paginate(10),
]);
}
public function edit(Company $company)
{
//Route model binding means we don't need to use findorfail() here
return view('companies.edit', [
'company' => $company,
'logos' => Logo::all()
]);
}
public function update(Company $company)
{
$attributes = $this->validateCompany($company);
$company->update($attributes);
return redirect()->route('companies.show', [
'company' => $company,
])->with('success', $company->name . ' Updated!');
}
public function destroy(Company $company){
$company->employees()->delete();
$company->delete();
return redirect()->route('companies.index')
->with('success', $company->name . ' and Associated Employees Deleted!');
}
public function validateCompany(?Company $company = null): array
{
$company ??= new Company();
return request()->validate([
'name' => 'required|max:255',
'email' => ['required', 'max:255',
'regex:/(?i)^([A-z\d\.-]+)@([A-z\d-]+)\.([A-z]{2,8})(\.[A-z]{2,8})?$/',
Rule::unique('companies', 'email')->ignore($company)],
'logos' => 'required',
'website' => ['required', 'max:255',
'regex:/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&=]*)/',
Rule::unique('companies', 'website')->ignore($company)]
]);
}
}