Web Frameworks [OGI06i]

02. My First Laravel App

02.1
Routing and Controllers

Routing

  • The routes of your web app are defined in routes/web.php*
  • The routes of your web API are defined in routes/api.php (URLs with prefix api/)*
  • What's a route?
    • A route is a combination of a pattern and an HTTP method
    • A route is handled by a handling function which returns/does x/y
  • Router methods very similar to bramus/router
  • Router method available for any HTTP verb (get, post, put, ...)**
    Route::get('/greeting', function () {
        return 'Hello World';
    });
  • Want an overview of all your routes? php artisan route:list
* you can change this behaviour in bootstrap/app.php
** Note: only GET and POST are allowed in plain web forms

Routing: Example

Route::get('/', function () {
	// show something (e.g. return a blob of HTML)
});

Route::get('/about', function () {
	// show something else (e.g. return a blob of HTML)
});

Route::post('/', function () {
	// do something (e.g. insert into database, send e-mail)
});
Note: POST requests are only accepted when a CSRF token is included in the request - we'll talk about that later on

Routing: HTTP Method Combinations

  • HTTP verb combinations
    Route::match(['get', 'post'], '/', function () { …
    
    Route::any('/foo', function () { …

Dynamic Routing

  • Working with Route parameters
    • The parameters will be passed into your route's Closure
    Route::get('/posts/{post}/comments/{comment}', function (string $postId, string $commentId) {
        return 'Post ' . $postId;
    });
  • Optional parameters
    Route::get('/user/{name?}', function (?string $name = 'John') {
        return $name;
    });
Important: in these examples we do not prevent XSS

Routing: Regex Constraints

  • Constrain parameters using the where method
    Route::get('/user/{id}/{name}', function (string $id, string $name) {
        //
    })->where(['id' => '[0-9]+', 'name' => '[a-zA-Z]+']);
  • Same, with helper methods:
    Route::get('/user/{id}/{name}', function (string $id, string $name) {
        //
    })->whereNumber('id')->whereAlpha('name');
  • You can add global constraints too
    • For example: for each id parameter in a dynamic route
    • Location: boot method of your AppServiceProvider (typically app/Providers/AppServiceProvider.php)

The HTTP Request (1)

  • The HTTP Request
    • is an instance of Illuminate\Http\Request
    • is typically obtained in your code by type hinting + dependency injection:
      use Illuminate\Http\Request;
      
      Route::post('/register', function(Request $request) {
          $email = $request->input('email', 'not provided');
          $name = $request->cookie('name');
          $value = $request->header('X-Header-Name');
          $iKnowItsTrue = $request->isMethod('post');
          $iKnowItsFalse = $request->is('register/*');
          $urlWithQueryString = $request->fullUrl();
          $ipAddress = $request->ip();
          if ($request->has('name')) {
              $name = $request->name;
              …
      });
    • the input() method handles any type of input parameter: POST parameters, GET parameters, ... and even JSON input (if the Content-Type is set properly)
From now on, let's call POST parameters payload parameters and GET parameters query string parameters

The HTTP Request (2)

  • The HTTP Request
    • use a . to dig into arrays (whether JSON or not)
    • $name = $request->input('products.0.name');
      $names = $request->input('products.*.name');	
    • dynamic properties
      $name = $request->name; // same as: $request->input('name');
    • handling file uploads
      if ($request->hasFile('additional_documents')) {
          $files = $request->file('additional_documents');
          foreach ($files as $file) {
              $path = $file->store('users/' . $userId . '/docs'); // folder relative to Filesystem root
              // assigns a unique ID as file name
              // want to specify file name? $file->storeAs()
          }
      }

The HTTP Request (3)

  • The HTTP Request
    • more on input parameters
    if ($request->has(['name', 'email'])) { ...        // if all of the specified parameters are present
    if ($request->hasAny(['name', 'email'])) { ...     // if any of the specified parameters are present
    if ($request->filled('name')) { ...                // if the parameter is present and not empty
    $input = $request->all();                          // array with all parameters
    $input = $request->only(['username', 'password']); // same, but only with specified paramters (if present)
    $input = $request->except(['credit_card']);        // same, without specified paramters
    $request->merge(['votes' => 0]);            // merge parameters with the request object (will overwrite if present)
    $input = $request->collect();               // collection with all parameters
    $name = $request->query('name');            // get the name parameter from the query string, not from the payload
    
    $archived = $request->boolean('archived');  // convert to boolean cf. "on" "yes"
    $birthday = $request->date('birthday');     // convert to a Carbon date
    $name = $request->string('name')->trim();   // convert to a Stringable and trim

The HTTP Response (1)

  • The HTTP Response
    • is an instance of Illuminate\Http\Response
    • can be returned in your code
      • as a string (automatic conversion):
        return 'Hello World';
      • as a JSON response:
        return ['name' => 'Abigail', 'state' => 'CA'];
      • idem, with helper (no use-statement needed):
        return response($content, $status));
      • idem, with chained methods:
        return response('Hello World', 200)
                        ->header('Content-Type', 'text/plain')
                        ->cookie('name', 'value');

The HTTP Response (2)

  • The HTTP Response
    • is an instance of Illuminate\Http\Response
    • can be returned in your code
      • as a view (automatic conversion):
        return view('greeting', ['name' => 'James']);
        — it involves rendering the resources/views/greeting.blade.php template
      • idem, with chained HTTP response methods:
        return response()->view('greeting', $data)->…;
      • JSON, with chained HTTP response methods:
        return response()->json(['name' => 'Abigail', 'state' => 'CA'])->…;
        json() also sets the HTTP Content-Type header

The HTTP Response (3)

  • The HTTP Response
    • is an instance of Illuminate\Http\Response
    • can be returned in your code
      • as a forced file download:
        return response()->download($pathToFile);
      • as a file displayed in the browser:
        return response()->file($pathToFile);
      • as a RedirectResponse instance:
        return redirect('home/dashboard');
      • idem, after invalid form submission:
        return back()->withInput();

Controllers

  • Create a *Controller in the folder app/Http/Controllers
    php artisan make:controller TaskController
  • Point to its methods in /routes/web.php (or in /routes/api.php)
    use App\Http\Controllers\TaskController;
    
    Route::get('/tasks', [TaskController::class, 'index']);
    Route::post('/tasks/create', [TaskController::class, 'store']);
    Route::post('/tasks/{task}/delete', [TaskController::class, 'destroy'])->whereNumber('task');
  • Write the appropriate method(s) in *Controller
    namespace App\Http\Controllers;
    use Illuminate\Http\Request;
    
    class TaskController extends Controller
    {
        public function index(Request $request) : View {
            $tasks = DB::select('select * from tasks where user_id = ?',
                                [$request->user_id]);
            return view('tasks.index', ['tasks' => $tasks]);
        }
    
        public function destroy(Request $request, string $id) : RedirectResponse

Route groups

  • Specify a route (URI) prefix
    Route::prefix('/admin')->group(function () {
        Route::get('/users', [AdminController::class, 'getUsers']);      // Matches the "/admin/users" URL
        Route::get('/dashboard', [AdminController::class, 'dashboard']); // Matches the "/admin/dashboard" URL
    });
  • Controller groups
    Route::controller(OrderController::class)->group(function () {
        Route::get('/orders/{id}', 'show');
        Route::post('/orders', 'store');
    });
  • Specify wildcard sub-domain for a route group
    Route::domain('{account}.myapp.com')->group(function () {
        Route::get('/user/{id}', function (string $account, string $id) {
            //
        });
    });

Custom HTTP error pages

  • HttpExceptions describe HTTP error codes from the server
    • e.g. page not found 404 (when there is no matching route), internal error (caused by developer) 500, …
    • raise them yourself by the abort() helper
    • these exceptions are rendered by the exception handler which by default
      • renders a custom error page if you define: resources/views/errors/404.blade.php for error code 404 and so on
      • PHP errors are rendered by a default exception page (if your app is in debug mode) and logged (by default to storage/logs/laravel.log)

02.2
Views and templates

Views

  • Presentation logic: "templates" stored in resources/views
    • A simple view using the Blade syntax e.g. resources/views/subdir/greeting.blade.php
      <html>
          <body>
              <h1>Hello, {{ $name }}</h1>
          </body>
      </html>
    • The view helper returns an instance of Illuminate\View\View, to which you can pass variables.
      return view('subdir.greeting', ['name' => 'Eddy']);
Note: the passed variables are stored in the instance, but the view is not rendered upon object creation.

Blade templates

  • Blade templates are compiled by the Blade template engine
    • File extension: .blade.php
    • File location, view instantiation (omit .blade.php!) and variable passing: see previous slide (Views).
    • Compiled into plain PHP code and cached until modified
    • → you will be debugging compiled views in storage/framework/views
    • Blade syntax very different from Twig, but closer to PHP

Blade syntax (1)

  • Displaying data
    Hello, {{ $name }}.
    • {{ Statements }} automatically sent through htmlentities to prevent XSS-attacks.
    • Mind the $
    • Any PHP code can be included e.g. {{ time() }} and forget about Twig filters
    • {{ $name or 'Default' }} is syntactic sugar for {{ isset($name) ? $name : 'Default' }}
    • Unescaped statements:
      Hello, {!! $name !!}.

Blade syntax (2)

  • Control structures
    @if (count($records) === 1)
        I have one record!
    @elseif (count($records) > 1)
        I have multiple records!
    @else
        I don't have any records!
    @endif
    
    @foreach ($events as $event)
        <p>This is user {{ $event->id }} ({{ $loop->iteration }} / {{ $loop->count }})</p>
    @endforeach
    • Structures don't work without ()
    • Check the docs for more syntax information
    • Associative array by index: just $event['id']
    • Object fields: just $event->id

Template Inheritance

  • Template inheritance: sections
    <html>
        <head>
            <title>App Name - @yield('title')</title>
        </head>
        <body>
            @section('sidebar')
                This is the master sidebar.
            @show
            <div class="container">
                @yield('content')
            </div>
        </body>
    </html>
    @extends('layouts.master')
    
    @section('title', 'Page Title')
    
    @section('sidebar')
        @parent
        <p>This is appended to the master sidebar.</p>
    @endsection
    
    @section('content')
        <p>This is my body content.</p>
    @endsection

Including Subviews

  • Control structures
    <div>
        @include('shared.errors')
    
        <form>
            <!-- Form Contents -->
        </form>
    </div>
    • Pass additional(!) data: @include('view.name', ['status' => 'complete'])
    • Many variants e.g. @includeWhen($boolean, 'view.name', ['status' => 'complete'])
    • Associative array by index: just @each('view.joboffer', $jobs, 'job')
    • All template variables are passed to the @include template, but NOT if you use @each

Blade with Javascript

  • Render an array as JSON
    <script>
        var app = {{ Js::from($array) }};
    </script>
  • Generate @ and curly braces for client-side templating
    <h1>Laravel</h1>
    @@if( condition )
    Hello, @{{ name }}.

Blade vs. clean URLs

  • Linking from route/1/ to route/2/ would generate a browser link to route/1/route/2 → helpers
    • asset() - Use {{ asset('css/common.css') }} to generate a path in your template to an asset in your public folder
    • url() - Use {{ url('artists/' . $id) }} to generate a path to a route
    • route() - Use {{ route('admin::dashboard') }} to generate a path to a named route

02.3
Coding practices

Coding practices

  • Laravel has many (hidden) features, libraries, syntactic sugar, recipes …
    • providing elegant solutions to many common problems
  • So stop thinking in plain PHP when writing code — how?
    • by getting introduced to these features (next slides)
    • by reading the docs and good code examples
    • by experience

#1 Facades and helpers

#2 Collections

#3 Carbon

  • Laravel comes with Carbon
    use Carbon\Carbon;
    • The Carbon class extends the PHP DateTime class
    • Very extensive API (read the docs !)
      $dt = Carbon::yesterday();
      
      echo $dt->diffInMinutes($dt->copy()->addWeekDays(50));
      
      Carbon::setLocale('de');
      echo Carbon::now()->addYear()->diffForHumans();    // in 1 Jahr
      

02.4
Getting started with databases

Recap: config

Raw SQL Queries (1)

  • In Laravel you can run raw SQL queries
    $users = DB::select('select * from users where active = ?', [1]);
    
    foreach ($users as $user) {
        echo $user->name;
    }
    • Parameter binding prevents against SQL injection
    • DB::select returns an array of StdClass objects with dynamic properties (column name or SQL alias)
    • Named bindings
      $results = DB::select('select * from users where id = :id', ['id' => 1]);

Raw SQL Queries (2)

  • In Laravel you can run raw SQL queries
    • Retrieval of scalar values
      $count = DB::scalar('select count(*) from users');
    • Similar functions for insert/update/delete
      $affected = DB::update('update users set votes = 100 where name = ?', ['John']);
    • General statements
      DB::statement('drop table users');

02.5
Migrations

Migrations (1)

  • Typical dev team situation
    • All team members have a local (test) instance of the database
    • From time to time the DB schema is modified
    • How can we easily share these changes?
      • By changing the global SQL-dump and put it to version control? Maybe, but it doesn't scale
      • By adding an SQL-update file and put it to version control? Maybe, but it is difficult to produce and it is not structured
  • Solution: Database Migrations
    • Easily modify and share the application's database schema.
    • It's like version control for your DB

Migrations (2)

  • How does it work?
    • A modification to the DB schema is described in a migration class file, named with a timestamp, located in database/migrations
    • A migration class contains two methods: up (perform migration) and down (reverse).
    • These modification are programmed with Laravel schema builder
    • Artisan helps you to create blueprints of migration classes
    • Put successful migrations to version control
    • In one Artisan command, you can execute all migrations that have not been executed before (logged in the DB table migrations)
  • Read more about it in the Laravel docs on Migrations

Migrations (3)

  • Example: table creation
    $ php artisan make:migration create_events_table
    • generates database/migrations/<timestamp>_create_events_table.php (auto-guessing it's a table creation)
      return new class extends Migration
      {
          public function up(): void
          {
              Schema::create('events', function (Blueprint $table) {
                  $table->id(); // your table has a PK auto-inc unsigned bigint column id
                  $table->timestamps(); // your table has created_at and updated_at columns
              });
          }
      
          public function down(): void
          {
              Schema::dropIfExists('events');
          }
      }
Note: if you create the classes without Artisan you need to call composer dump-autoload

Migrations (4)

  • Example: table creation
    • complete the schema creation
      public function up(): void
      {
          Schema::create('events', function (Blueprint $table) {
              $table->id(); // LEAVE IT HERE
              $table->string('title');
              $table->text('description');
              $table->date('event_date');
      
              // foreign key: column types need to be EXACTLY the same
              $table->unsignedBigInteger('location_id');
              $table->foreign('location_id')->references('id')->on('locations');
              // all at once: $table->foreignId('location_id')->constrained();
      
              $table->timestamps(); // LEAVE IT HERE
          });
      }
    • run all migrations
      $ php artisan migrate

Migrations (5)

  • Let's get practical
    • Consult the tables in the migrations docs on available column types, column modifiers and index modifiers
    • After all, there exists a mapping for each command, data type, … to each of the supported DBMS systems
    • More flavours of php artisan migrate
        migrate:fresh     Drop all tables and re-run all migrations
        migrate:install   Create the migration repository
        migrate:refresh   Reset and re-run all migrations
        migrate:reset     Rollback all database migrations
        migrate:rollback  Rollback the last database migration (option: --step=2)
        migrate:status    Show the status of each migration
        migrate --pretend Show SQL queries to be executed without running them
      
    • Still stuck? Delete the migrations table
    • Too many migrations? Squash them.

Questions?

Sources