- Create a new Laravel project
- Hello world exercise
- Route parameter exercise
- Controller exercise
- View exercise
- Render list exercise
- Layout exercise
- Register form
- Ticketing system app
composer create-project --prefer-dist laravel/laravel name
Create a get route for the hello
URL and return Hello, world
.
- Open routes/web.php
- Use Route facade
Route::get(url, closure);
Solution
Route::get('hello', function() {
return 'Hello, world';
});
- Create
hello
get route and accept aname
parameters as string - Return name parameter
- Open routes/web.php
- Use Route facade
- Define
{name}
as parameter
Solution
Route::get('hello/{name}', function(string $name) {
return $name;
});
- Generate a controller
- Register a route that accepts two numbers as parameters
- Return the sum of the parameters
php artisan make:controller ControllerName
- Use Route facade
- Define
{a}
and{b}
as parameters
Solution
terminal
php artisan make:controller ExampleController
routes/web.php
Route::get('{a}/{b}', 'ExampleController@sum');
app/http/Controllers/ExampleController
public function sum(int $a, int $b) {
return $a + $b;
}
- Create a get route and accept
name
as parameter - Return a view with and pass the name parameter
- Render
Hello,
and the name
- Use Route facade
- Define
{name}
as parameter - Create a blade file inside
resources/views
folder - Return the blade file from the previous step using
view('view_name', ['name' => $name])
helper
Solution
routes/web.php
Route::get('{name}', function(string $name) {
return view('hello', ['name' => $name])
});
resources/views/hello.blade.php
<h1>Hello, {{$name}}</h1>
Star Wars movies data
$starWarsMovies = [
'Episode IV – A New Hope (1977)',
'Episode V – The Empire Strikes Back (1980)',
'Episode VI – Return of the Jedi (1983)',
'Episode I – The Phantom Menace (1999)',
'Episode II – Attack of the Clones (2002)',
'Episode III – Revenge of the Sith (2005)',
'Episode VII – The Force Awakens (2015)',
'Episode VIII – The Last Jedi (2017)',
'Episode IX – The Rise of Skywalker (2019)',
];
- Create a blade template and render
Star Wars movies
- Create a route, return the template and pass Star wars movies
- Use unordered list element
ul
- Render iteration number before each movie
- Add
first-movie
class on the first movie - Render last movie inside a
strong
element
- Use Route facade
- Define
{name}
as parameter - Create a blade file inside
resources/views
folder - Return the blade file from the previous step using
view('view_name', ['name' => $name])
helper @foreach($array as $item) @endforeach
- Use
$loop
variable inside foreach- index
- iteration
- remaining
- first
- last
Solution
routes/web.php
Route::get('movies', function() {
$starWarsMovies = [
'Episode IV – A New Hope (1977)',
'Episode V – The Empire Strikes Back (1980)',
'Episode VI – Return of the Jedi (1983)',
'Episode I – The Phantom Menace (1999)',
'Episode II – Attack of the Clones (2002)',
'Episode III – Revenge of the Sith (2005)',
'Episode VII – The Force Awakens (2015)',
'Episode VIII – The Last Jedi (2017)',
'Episode IX – The Rise of Skywalker (2019)',
];
return view('movies', ['movies' => $starWarsMovies])
});
resources/views/movies.blade.php
<ul>
@foreach($movies as $movie)
<li class="{{$loop->first ? 'first-movie' : ''}}">
@if(!$loop->last)
{{$loop->iteration}} - {{$movie}}
@else
<strong>{{$loop->iteration}} - {{$movie}}</strong>
@endif
</li>
@endforeach
<ul>
- Create a view named
layout
and declare 2 sections- title (Document title)
- content
- Include Bootstrap using the CDN link
- Create another view named
home
- Extend
layout
view and overridetitle
andcontent
sections - Add a button inside
content
section with the classesbtn btn-primary
- Extend
- Create a route and return the
home
view
@yield('section_name')
@extends('view_name')
@section('section_name') @endsection
- Bootstrap:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
Solution
resources/views/layout.blade.php
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<div>
@yield('content')
</div>
</body>
</html>
resources/views/home.blade.php
@extends('layout')
@section('title', 'Home page')
@section('content')
<button class="btn btn-primary"></button>
@endsection
routes/web.php
Route::get('home', function() {
return view('home');
});
- Create a controller named
RegisterController
- Create a view named
register
- Define a
/register
get and post routes - Form requirements
- Name field
- Email field
- Password field
- Password confirmation
- Action to
RegisterController@register
- Form errors
- Old values
- Implement registerForm method on
RegisterController
and returnregister
view - Implement register method on
RegisterController
- Validation
- Name rules -> Required, String, Max 50
- Email rules -> Required, String, Email, Max 255
- Password rules -> Required, Min 8, Confirmed
- Validation
- Create a view named
welcome
and renderHello, {{$name}}
- Create a method on controller named
welcome
- Define a
/welcome/{name}
route - On succusfull registration redirect to
welcome
and pass thename
as parameter
php artisan make:controller ControllerName
- Use
Route
facade to define routes action('ControllerName@method')
@csrf
- Name attribute on form fields
@method()
request->validate(['field_name' => 'rule1|rule2'])
redirect(url)
action('ControllerName@method', ['param' => $param])
Solution
routes/web.php
Route::get('/register', 'RegisterController@registerForm');
Route::post('/register', 'RegisterController@register');
Route::get('/welcome/{name}', 'RegisterController@welcome');
app/Http/Controllers/RegisterController
class RegisterController extends Controller
{
public function registerForm()
{
return view('register');
}
public function register(Request $request)
{
$validatedData = $request->validate(
[
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]
);
return redirect(action('RegisterController@welcome', ['name' => $validatedData['name']]));
}
public function welcome(string $name)
{
return view('welcome', ['name' => $name]);
}
}
resources/views/register.blade.php
<ul>
@foreach($errors->all as $error)
<li>{{$error}}</li>
@endforeach
</ul>
<form action="{{action('RegisterController@register')}}" method="POST">
@csrf
<input type="text" name="name" placeholder="Name" value="{{old('name', '')}}">
<input type="email" name="email" placeholder="Email" value="{{old('email', '')}}">
<input type="password" name="password" placeholder="Password" value="{{old('password', '')}}">
<input type="password" name="password_confirmation" placeholder="Password confirmation">
<input type="submit" value="Register">
</form>
resources/views/register.blade.php
<h1>Hello, {{$name}}</h1>
git clone --branch installation https://github.com/SocialNerdsGR/laravel-workshop-ticketing-system.git ticketing-app
cd ticketing-app
chmod +x checkout.sh
cp .env.example .env
composer install
php artisan key:generate
touch database/database.sqlite
Change DB_CONNECTION variable on .env file from DB_CONNECTION=mysql to DB_CONNECTION=sqlite
- Generate Model and Migration files using artisan
- Ticket fields
- type: unsignedBigInteger, name: user_id (foreign key)
- type: string, name: title
- type: text, name: text
- Create 2 tickets using
Tinker
- Use
Ticket::create()
method
- Use
- User relationship on Ticket model
php artisan make:model Name --migration (-m)
foreign('field_name')->references('table_id')->on('table')
php artisan migrate
php artisan tinker
protected $fillable = ['field_name', 'field_name']
return $this->belongsTo(Model::class)
Solution
create_tickets_table.php
public function up()
{
Schema::create('tickets', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
$table->string('title');
$table->text('content');
$table->timestamps();
});
}
Ticket.php
class Ticket extends Model
{
protected $fillable = ['title', 'content', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
}
chmod +x checkout.sh && ./checkout.sh ticket-resource
- Create a new folder named
tickets
inside resources - Create
index.blade.php
file - Implement
index
method on TicketsController- Load tickets using
paginate()
method - Return
index
view passing tickets as parameter
- Load tickets using
- Render tickets
- Extend app layout
- title -> link to /tickets/{ticketId} using
action
helper - render pagination links using
links()
method
Ticket::paginate(PER_PAGE)
view('view_name', ['key' => $value])
@extends('view_name')
@section('section_name') @endsection
@foreach
action('ControllerName@method', ['key' => $value])
Solution
TicketsController.php
public function index()
{
$tickets = Ticket::paginate(10);
return view('tickets.index', ['tickets' => $tickets]);
}
resources/tickets/index.blade.php
@extends('layouts.app')
@section('content')
<h2>Tickets</h2>
<ul>
@foreach($tickets as $ticket)
<li><a href={{action('TicketsController@show', ['ticket' => $ticket->id])}}>{{$ticket->title}}</a></li>
@endforeach
</ul>
{{$tickets->links()}}
@endsection
chmod +x checkout.sh && ./checkout.sh render-tickets
- Create a link on
index.blade.php
targeting create method on TicketsController usingaction
helper - Create
create.blade.php
inside tickets folder - Extend layout
- Implement new ticket form
- form
- method
- action
- Title input
- Contnent textarea
- Submit button
@extend('view_name')
action('ControllerName@method')
@csrf
- Name attribute on form fields
Solution
resources/tickets/index.blade.php
<a href={{action('TicketsController@create')}}>Create new ticket</a>
TicketsController.php
public function create()
{
return view('tickets.create');
}
resources/tickets/create.blade.php
@extends('layouts.app')
@section('content')
<form method="POST" action={{action('TicketsController@store')}}>
@csrf
<input type="text" name="title" placeholder="Title">
<textarea placeholder="Ticket" name="content" cols="30" rows="10"></textarea>
<input type="submit" value="Create">
</form>
@endsection
chmod +x checkout.sh && ./checkout.sh ticket-controller-store
- Render form errors
- Fill old form values
- Redirect to created ticket
- Use
$errors
object to get errors - Use
old('field_name', defaultValue)
helper to get form values - Use
action
helper to generate a link
Solution
resources/tickets/create.blade.php
@section('content')
<ul>
@foreach($errors->all() as $error)
<li>{{$error}}</li>
@endforeach
</ul>
<form method="POST" action={{action('TicketsController@store')}}>
@csrf
<input value="{{old('title', '')}}" type="text" name="title" placeholder="Title">
<textarea placeholder="Ticket" name="content" cols="30" rows="10">{{old('content', '')}}</textarea>
<input type="submit" value="Create">
</form>
@endsection
TicketsController.php
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|min:5|unique:tickets',
'content' => 'required|max:255'
]);
$ticket = Ticket::create([
'user_id' => Auth::user()->id,
'title' => $validatedData['title'],
'content' => $validatedData['content']
]);
return redirect(action('TicketsController@show', ['ticket' => $ticket->id]));
}
chmod +x checkout.sh && ./checkout.sh ticket-create-form-errors
- Create
show.blade.php
file - Implement
show
method on TicketsController- Return
show
view passing ticket as parameter
- Return
- Render ticket
- Title
- Content
view('view_name', ['key' => $value])
@extends('view_name')
@section('section_name') @endsection
Solution
TicketsController.php
public function show(Ticket $ticket)
{
return view('tickets.show', ['ticket' => $ticket]);
}
resources/tickets/show.blade.php
@extends('layouts.app')
@section('content')
<h3>{{$ticket->title}}</h3>
<p>
{{$ticket->content}}
</p>
@endsection
chmod +x checkout.sh && ./checkout.sh ticket-show-view
- Create delete form after ticket title on
show
view - Use delete method
- Implement destroy method on
TicketsController
- Redirect back to
index
@method()
@csrf
$model->delete()
action('ControllerName@method')
Solution
resources/tickets/show.blade.php
<form method="POST" action="{{action('TicketsController@destroy', ['ticket' => $ticket->id])}}">
@csrf
@method('DELETE')
<input type="submit" value="Delete">
</form>
TicketsController.php
public function destroy(Ticket $ticket)
{
$ticket->delete();
return redirect(action('TicketsController@index'));
}
chmod +x checkout.sh && ./checkout.sh delete-ticket
- Add edit link on
show
view - Create
edit.blade.php
file - Implement
edit
method on TicketsController- Return
edit
view passing ticket as parameter
- Return
- Render edit form
- Use
PATCH
method - Title input
- Content
- Use
- Render errors and old values
- Validate and update ticket
- Redirect to
show
method
@extend('view_name')
action('ControllerName@method')
@csrf
@method()
- Name attribute on form fields
request->validate(['field_name' => 'rule1|rule2'])
$model->update($validatedData)
Solution
resources/tickets/show.blade.php
<a href="{{action('TicketsController@edit', ['ticket' => $ticket->id])}}">Edit ticket</a>
TicketsController.php
public function edit(Ticket $ticket)
{
return view('tickets.edit', ['ticket' => $ticket]);
}
resources/tickets/edit.blade.php
@extends('layouts.app')
@section('content')
<ul>
@foreach($errors->all() as $error)
<li>{{$error}}</li>
@endforeach
</ul>
<form method="POST" action="{{action('TicketsController@update', ['ticket' => $ticket->id])}}">
@csrf
@method('PATCH')
<input name="title" value="{{old('title', $ticket->title)}}" type="text">
<textarea name="content" cols="30" rows="10">{{old('content', $ticket->content)}}</textarea>
<input type="submit" value="Save">
</form>
@endsection
TicketsController.php
public function update(Request $request, Ticket $ticket)
{
$validatedData = request()->validate([
'title' => 'required|min:5|unique:tickets,title,' . $ticket->id,
'content' => 'required|max:255'
]);
$ticket->fill($validatedData);
$ticket->save();
return redirect(action('TicketsController@edit', ['ticket' => $ticket->id]));
}
chmod +x checkout.sh && ./checkout.sh delete-ticket-policy
- Implement
update
method onTicketPolicy
- Render edit link on show view, only if user is authorized to update the ticket
@can('model', $model)...@endcan
Solution
TicketPolicy.php
public function update(User $user, Ticket $ticket)
{
return $user->id == $ticket->user_id;
}
resources/tickets/show.blade.php
@can('update', $ticket)
<a href="{{action('TicketsController@edit', ['ticket' => $ticket->id])}}">Edit ticket</a>
@endcan
chmod +x checkout.sh && ./checkout.sh update-ticket-policy
- Generate Model and Migration files using artisan
- Reply fields
- type: unsignedBigInteger, name: user_id (foreign key)
- type: unsignedBigInteger, name: ticket_id (foreign key)
- type: text, name: reply
- Create 2 replies using
Tinker
- Use
Reply::create()
method
- Use
php artisan make:model Name --migration (-m)
foreign('field_name')->references('table_id')->on('table')
php artisan migrate
php artisan tinker
protected $fillable = ['field_name', 'field_name']
Solution
Reply.php
class Ticket extends Model
{
protected $fillable = ['title', 'content', 'user_id'];
}
database/migrations/create_replies_table.php
Schema::create('replies', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('ticket_id');
$table->foreign('ticket_id')->references('id')->on('tickets');
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
$table->text('reply');
$table->timestamps();
});
chmod +x checkout.sh && ./checkout.sh reply-model
- Create replies relationship on
Ticket
model - Create user relationship on
Reply
model
hasMany(Model::class)
belongsTo(Model::class)
Solution
Ticket.php
class Ticket extends Model
{
protected $fillable = ['title', 'content', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function replies()
{
return $this->hasMany(Reply::class);
}
}
Reply.php
class Reply extends Model
{
protected $fillable = ['user_id', 'ticket_id', 'reply'];
public function user()
{
return $this->belongsTo(User::class);
}
}
chmod +x checkout.sh && ./checkout.sh reply-model-relationships
- Pass paginated (5 per page) replies on
show
view fromTicketsController
- Render replies
- Author name
- Reply (Content)
- Created_at formatted ('D m M Y')
- Pagination links
- Get relationship data
Model::relationship()->paginated(10)
@foreach @endforeach
$model->links()
to render pagination links
Solution
TicketsController.php
public function show(Ticket $ticket)
{
$replies = $ticket->replies()->paginate(5);
return view('tickets.show', compact('ticket', 'replies'));
}
resources/tickets/show.blade.php
@extends('layouts.app')
@section('content')
<h3>{{$ticket->title}}</h3>
@can('update', $ticket)
<a href="{{action('TicketsController@edit', ['ticket' => $ticket->id])}}">Edit ticket</a>
@endcan
@can('delete', $ticket)
<form method="POST" action="{{action('TicketsController@destroy', ['ticket' => $ticket->id])}}">
@csrf
@method('DELETE')
<input type="submit" value="Delete">
</form>
@endcan
<p>
{{$ticket->content}}
</p>
<div>
@foreach($replies as $reply)
<div>
<h5>Author: {{$reply->user->name}}</h5>
<strong>{{$reply->created_at->format('D m M Y')}}</strong>
<p>{{$reply->reply}}</p>
</div>
@endforeach
{{$replies->links()}}
</div>
@endsection
Reply.php
class Reply extends Model
{
protected $fillable = ['user_id', 'ticket_id', 'reply'];
public function user()
{
return $this->belongsTo(User::class);
}
}
chmod +x checkout.sh && ./checkout.sh latest-replies
- Create replies resource controller
- Remove all methods except store and destroy
- Register a nested resource route only for store and destroy methods
tickets.replies
- Create new reply form on tickets.show view
- @csrf
- Reply text area
- Action to
RepliesController@store
- Validate reply
- Rules -> required, maximum characters 255
- Save reply
- Handle old values on error
- Redirect
back
after save
php artisan make:controller ControllerName --resource --model=ModelName
Route::resource('path.nested', 'ControllerName')
action('ControllerName@method')
$request->validate(['field_name' => 'rule1|rule2'])
old('field_name', $defaultValue)
- Redirect back using
back()
helper
Solution
web.php
Route::middleware(['auth'])->group(function () {
Route::resource('tickets', 'TicketsController');
Route::resource('tickets.replies', 'RepliesController')->only(['store', 'destroy']);
});
resources/views/tickets/show.blade.php
@extends('layouts.app')
@section('content')
<h3>{{$ticket->title}}</h3>
@can('update', $ticket)
<a href="{{action('TicketsController@edit', ['ticket' => $ticket->id])}}">Edit ticket</a>
@endcan
@can('delete', $ticket)
<form method="POST" action="{{action('TicketsController@destroy', ['ticket' => $ticket->id])}}">
@csrf
@method('DELETE')
<input type="submit" value="Delete">
</form>
@endcan
<p>
{{$ticket->content}}
</p>
// Create Reply Form
<form method="POST" action="{{action('RepliesController@store', ['ticket' => $ticket->id])}}">
@csrf
<textarea class="" name="reply" cols="30" rows="10">{{old('reply', '')}}</textarea>
<input type="submit" value="Reply">
</form>
// End
<div>
@foreach($replies as $reply)
<div>
<h5>Author: {{$reply->user->name}}</h5>
<strong>{{$reply->created_at->format('D m M Y')}}</strong>
<p>{{$reply->reply}}</p>
</div>
@endforeach
{{$replies->links()}}
</div>
@endsection
RepliesController
public function store(Request $request, Ticket $ticket)
{
$validatedData = $request->validate([
'reply' => 'required|max:255'
]);
Reply::create([
'reply' => $validatedData['reply'],
'user_id' => Auth::user()->id,
'ticket_id' => $ticket->id
]);
return back();
}
chmod +x checkout.sh && ./checkout.sh store-reply
- Create delete form on each reply
- @csrf
- Delete method
- Action to RepliesController@destroy
- Implement destroy method on
RepliesController@destroy
- Redirect back
@method('DELETE')
action('ControllerName@method', ['reply' => $id])
$model->delete()
back()
Solution
resources/views/tickets/show.blade.php
@extends('layouts.app')
@section('content')
<h3>{{$ticket->title}}</h3>
@can('update', $ticket)
<a href="{{action('TicketsController@edit', ['ticket' => $ticket->id])}}">Edit ticket</a>
@endcan
@can('delete', $ticket)
<form method="POST" action="{{action('TicketsController@destroy', ['ticket' => $ticket->id])}}">
@csrf
@method('DELETE')
<input type="submit" value="Delete">
</form>
@endcan
<p>
{{$ticket->content}}
</p>
<form method="POST" action="{{action('RepliesController@store', ['ticket' => $ticket->id])}}">
@csrf
<textarea class="" name="reply" cols="30" rows="10">{{old('reply', '')}}</textarea>
<input type="submit" value="Reply">
</form>
<div>
@foreach($replies as $reply)
<div>
// DELETE FORM
<form method="POST" action="{{action('RepliesController@destroy', ['ticket' => $ticket->id, 'reply' => $reply->id])}}">
@csrf
@method('DELETE')
<input type="submit" value="Delete">
</form>
// END
<h5>Author: {{$reply->user->name}}</h5>
<strong>{{$reply->created_at->format('D m M Y')}}</strong>
<p>{{$reply->reply}}</p>
</div>
@endforeach
{{$replies->links()}}
</div>
@endsection
RepliesController
public function destroy(Ticket $ticket, Reply $reply)
{
$reply->delete();
return back();
}
chmod +x checkout.sh && ./checkout.sh delete-reply
- Create ReplyPolicy
- Register ReplyPolicy on RepliesController
- Implement ReplyPolicy methods
- Show delete reply button if user is authorized to delete the reply
php artisan make:policy ModelNamePolicy --model=ModelName
$this->authorizeResource(ModelName::class, 'model_name');
@can('model_name', $model) @endcan
Solution
RepliesController
public function __construct()
{
$this->authorizeResource(Reply::class, 'reply');
}
ReplyPolicy.php
class ReplyPolicy
{
use HandlesAuthorization;
public function viewAny(User $user)
{
return false;
}
public function view(User $user, Reply $reply)
{
return false;
}
public function create(User $user)
{
return true;
}
public function update(User $user, Reply $reply)
{
return false;
}
public function delete(User $user, Reply $reply)
{
return $user->id == $reply->user_id;
}
public function restore(User $user, Reply $reply)
{
return false;
}
public function forceDelete(User $user, Reply $reply)
{
return false;
}
}
resources/views/show.blade.php
@extends('layouts.app')
@section('content')
<h3>{{$ticket->title}}</h3>
@can('update', $ticket)
<a href="{{action('TicketsController@edit', ['ticket' => $ticket->id])}}">Edit ticket</a>
@endcan
@can('delete', $ticket)
<form method="POST" action="{{action('TicketsController@destroy', ['ticket' => $ticket->id])}}">
@csrf
@method('DELETE')
<input type="submit" value="Delete">
</form>
@endcan
<p>
{{$ticket->content}}
</p>
<form method="POST" action="{{action('RepliesController@store', ['ticket' => $ticket->id])}}">
@csrf
<textarea class="" name="reply" cols="30" rows="10">{{old('reply', '')}}</textarea>
<input type="submit" value="Reply">
</form>
<div>
@foreach($replies as $reply)
<div>
// REPLY POLICY
@can('delete', $reply)
<form method="POST" action="{{action('RepliesController@destroy', ['ticket' => $ticket->id, 'reply' => $reply->id])}}">
@csrf
@method('DELETE')
<input type="submit" value="Delete">
</form>
@endcan
// END
<h5>Author: {{$reply->user->name}}</h5>
<strong>{{$reply->created_at->format('D m M Y')}}</strong>
<p>{{$reply->reply}}</p>
</div>
@endforeach
{{$replies->links()}}
</div>
@endsection