Szerveroldali webprogramozás

A tárgy ismertetése. Ismétlés. Függvénykönyvtárak használata.

Tűri Erik
doktorandusz
turierik@inf.elte.hu

dr. Horváth Győző
egyetemi docens
horvath.gyozo@inf.elte.hu

Kontextus

Előzmények

  • Webprogramozás tárgy
  • Statikus vs dinamikus
  • Kliens és szerver
  • Alap szintű, natív
  • Függvénykönyvtárak nélkül
  • Keretrendszerek nélkül

Kliens evolúció

  • Statikus weboldalak
  • 1995-2000
    • dinamikus weboldalak
    • JS, Java, Flash
    • 1. böngészőháború
  • 2000-2006
    • Sötét középkor
    • 2. böngészőháború

Kliens evolúció

  • 2006-2010
    • AJAX
    • JS nyelv újrafelfedezése
    • programozási minták
    • Cross-browser
    • Progresszív fejlesztés (unobtrusive JS)
    • 3. böngészőháború
  • 2010-2014
    • tervezési minták
    • MV*
  • 2014-
    • Komponensalapú fejlesztés

Szerver evolúció

  • Statikus weboldalak
  • 1995-2000
    • dinamikus weboldalak
    • JS (!), PHP, Perl
  • 2000-
    • hatalmas fejlődés, vállalati rendszerek
    • tervezési minták - MVC
    • Java (Enterprise - EJB, J2EE), ASP.NET
    • SOAP és XML alapú webszolgáltatások
  • 2000: REST - Roy Fielding disszertációja
  • 2001: CMS rendszerek gyökerei (Drupal, Wordpress)

Szerver evolúció

  • 2005: Microszervíz architektúrák
  • 2008: Node.js
  • 2011: WebSocket
  • 2015
    • HTTP/2
    • GraphQL
  • … tovább is van
    • Serverless/FaaS, Kubernetes, Edge Computing
    • gRPC, JAMStack, HTTP/3 QUIC
    • Go, Rust, Deno, stb.

Érdekességek

Modern web architektúra

Forrás

A tárgy ismertetése

Információk

https://canvas.elte.hu/

  • követelmények
  • információk
  • segédanyagok
  • előadásdiák
  • feladatsorok
  • számonkérés

A tárgyról

  • Szerveroldali webprogramozás
  • Architektúra
  • Tervezési minták
  • Technológia
  • Eszközök
  • Fogalmak
  • Nyelvfüggetlen implementáció

Tematika

  • Előadáson
    • MVC tervezési minta, keretrendszerek
    • Hitelesítés és jogosultságkezelés
    • Node.js és az aszinkron modell
    • REST API, GraphQL, Websocket, WebRTC
  • Gyakorlaton
    • klasszikus szerveroldali alkalmazás
    • API fejlesztés: REST, GraphQL
    • (Node.js, Websocket)
  • Bővebben lásd

Követelmények

  • Röviden
    • 1 db beadandó - félév közepén
    • 1 db API ZH - vizsgaidőszakban
    • előadás kvízek (5 db) - minimumkövetelmény
  • Bővebben lásd

Ismétlés

Web komponensei, szabványai

  • HTML
  • URL
  • HTTP

URL

HTTP protokoll

Dinamikus szerveroldali tartalom

A leküldendő tartalmat egy program állítja elő.

Architektúrák

Common Gateway Interface (CGI)

Azt határozza meg, hogy egy webszerver hogyan indíthat el egy programot és milyen módon cserél adatot vele.

  • Indítás: program futtatása
  • Adatok
    • környezeti változók
    • standard I/O
  • Program eredménye
    • standard kimeneten

Dinamikus szerveroldali webprogramozás feladatai

  • Program → HTML
  • Kimenet (HTML generálás)
  • Bemenet (link, űrlap)
  • Adattárolás, fájlkezelés
  • Munkamenet-kezelés
  • Hitelesítés és jogosultságkezelés
  • Kódszervezés
  • AJAX kiszolgálás, JSON kommunikáció

PHP

  • alacsony belépési küszöb
  • széles körben használt
  • folyamatosan fejlődő nyelv

Kimenet generálás

<?php 
// input (?)
$tracks = [
  ["id" => 1, "name" => "guitar", "muted" => false],
  ["id" => 2, "name" => "bass",   "muted" => true ],
  ["id" => 3, "name" => "vocal",  "muted" => false],
];

// processing
$enabledTracks = array_filter($tracks, function ($track) {
  return !$track["muted"];
});

// output
?>
<div id="tracks">
  <?php foreach($enabledTracks as $t) : ?>
    <div class="track">
      <?= $t["name"] ?>
    </div>
  <?php endforeach ?>
</div>

Input, űrlapfeldolgozás

<?php
// debug
print_r($_GET);
print_r($_POST);

// Segédfüggvény
function is_empty($input, $key) {
  return !(isset($input[$key]) && trim($input[$key]) !== '');
}

// business logic
function perimeter(float $radius): float {
  return 2 * $radius * pi();
}

function validate($input, &$data, &$errors) {
    // sugár vizsgálata
  $data['radius'] = null;
  if (is_empty($input, 'radius')) {
    $errors[] = 'A sugár megadása kötelező';
  } 
  else if (!is_numeric($input['radius'])) {
    $errors[] = 'A sugár nem szám!';
  }
  else {
    $data['radius'] = (float)$input['radius'];
  } 
  
  return !(bool)$errors;
}

$errors = [];
$input = $_GET;

if (count($_GET) !== 0) {
  // validation
  if (validate($input, $data, $errors)) {
    // input
    $sugar = $data['radius'];
    // processing
    $ker = perimeter($radius);
  }    
}

// output
?>
<?php if ($errors) : ?>
    <ul>
        <?php foreach($errors as $error) : ?>
            <li><?= $error ?></li>
        <?php endforeach; ?>
    </ul>
<?php endif; ?>

<form action="" method="GET">
    Radius: 
    <input type="text" name="radius" value="<?= $input["radius"] ?? "10" ?>">
    <button>Calculate</button>
</form>

<?php if (isset($ker)) : ?>
  <p>Sugár = <?php echo $radius; ?></p>
  <p>Kerület = <?php echo $ker; ?></p>
<?php endif; ?>

Adattárolás

  • File-alapú
  • JSON formátumban
  • Sorosítva: json_encode, json_decode
function load_from_file(string $filename, bool $array_result = false, $default_data = []) {
  $s = @file_get_contents($filename);
  return ($s === false 
    ? $default_data 
    : json_decode($s, $array_result));
}

function save_to_file(string $filename, $data) {
  $s = json_encode($data);
  return file_put_contents($filename, $s, LOCK_EX);
}

Munkamenet-kezelés, hitelesítés

  • Munkamenet
    • kliensek megkülönböztetése
    • kliensenkénti adattárolás
  • $_SESSION
  • Hitelesítés

Függvénykönyvtárak használata

Névterek

  • Definiálás
    • namespace \A\B\C
  • Használat
    • \A\B\C\func()

Névterek

// definition and local use
namespace Tools\Html;
class Table { /* ... */ }
$table = new Table();
// Use outside the namespace
$table = new Tools\Html\Table();

// Use inside the namespace
namespace Tools\Html;
$table = new Table();

// class alias
use Tools\Html\Table as T;
$table = new T();

// namespace alias
use Tools\Html as H;
$table = new H\Table();

// namespace alias
use Tools\Html;
$table = new Html\Table();

Függvénykönyvtárak

  • Külső függvénykönyvtárak használata, mert:
    • Nem kell mindent nekünk megírni!
    • Jól (jobban) megírt, tesztelt megoldások! (általában)

Composer

  • PHP csomagkezelője
  • Packagist: fő repozitórium
  • composer require csomagnév
  • vendor mappába (.gitignore❗)
  • composer.json
  • composer install: függőségek telepítése
  • Composer autoload
require 'vendor/autoload.php';

composer.json

{
    "$schema": "https://getcomposer.org/schema.json",
    "name": "laravel/laravel",
    "type": "project",
    "description": "The skeleton application for the Laravel framework.",
    "keywords": [
        "laravel",
        "framework"
    ],
    "license": "MIT",
    "repositories": [
        {
            "type": "composer",
            "url": "https://webprogramozas.inf.elte.hu/example-zh"
        },
        {
            "packagist.org": false
        }
    ],
    "require": {
        "php": "^8.2",
        "laravel/framework": "^12.0",
        "laravel/sanctum": "^4.1",
        "laravel/tinker": "^2.10.1",
        "mll-lab/laravel-graphiql": "^3.3",
        "nuwave/lighthouse": "^6.55"
    },
    "require-dev": {
        "fakerphp/faker": "^1.23",
        "laravel/pail": "^1.2.2",
        "laravel/pint": "^1.13",
        "laravel/sail": "^1.41",
        "mockery/mockery": "^1.6",
        "nunomaduro/collision": "^8.6",
        "phpunit/phpunit": "^11.5.3"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "post-autoload-dump": [
            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
            "@php artisan package:discover --ansi"
        ],
        "post-update-cmd": [
            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
        ],
        "post-root-package-install": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "@php artisan key:generate --ansi",
            "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
            "@php artisan migrate --graceful --ansi"
        ],
        "dev": [
            "Composer\\Config::disableProcessTimeout",
            "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
        ],
        "test": [
            "@php artisan config:clear --ansi",
            "@php artisan test"
        ]
    },
    "extra": {
        "laravel": {
            "dont-discover": []
        }
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": "dist",
        "sort-packages": true,
        "allow-plugins": {
            "pestphp/pest-plugin": true,
            "php-http/discovery": true
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}

NPM

  • Node.js (alapértelmezett) csomagkezelője
    • Laravelnél sem ússzuk meg, front-end assetek 😢
  • npmjs.com: fő repozitórium
  • npm install csomagnév / npm i csomagnév
  • node_modules mappába (.gitignore❗)
  • package.json
  • npm install / npm i: függőségek telepítése
  • CommonJS / ES6 importálás

Csomagok használata

  • névterekbe rendezve
  • namespace, use
// SleekDb.php
namespace SleekDB;
class SleekDB { /* ... */ }
// index.php
$myStore = \SleekDB\SleekDB::store('something', $dataDir);
// Or
// index.php, with using namespace
use \SleekDB\SleekDB;
$myStore = SleekDB::store('something', $dataDir);

Példa – tracklista

<?php
function get_all_tracks() {
  $tracks = json_decode(file_get_contents('tracks.array.json'), true);
  return $tracks;
}

$tracks = get_all_tracks();
?>
<ul>
  <?php foreach($tracks as $track) : ?>
    <li style="background-color: <?= $track['color'] ?>">
      <span>🎵 <?= $track['category'] ?></span>
      <?= $track['name'] ?> (<?= $track['instrument'] ?>)
    </li>
  <?php endforeach ?>
</ul>

Példa – tracklista

SleekDB: Flat file NoSQL-like database

<?php
require __DIR__ . '/vendor/autoload.php';

$dataDir = "./data";
$trackStore = \SleekDB\SleekDB::store('tracks', $dataDir);

$tracks = $trackStore->fetch();
?>
<ul>
  <?php foreach($tracks as $track) : ?>
    <li style="background-color: <?= $track['color'] ?>">
      <span>🎵 <?= $track['category'] ?></span>
      <?= $track['name'] ?> (<?= $track['instrument'] ?>)
    </li>
  <?php endforeach ?>
</ul>

Seed

require __DIR__ . '/vendor/autoload.php';

$dataDir = "./data";
$trackStore = \SleekDB\SleekDB::store('tracks', $dataDir);

// Delete store
$trackStore->deleteStore();
$trackStore = \SleekDB\SleekDB::store('tracks', $dataDir);

// Prepare users data.
$tracks = json_decode(file_get_contents('tracks.array.orig.json'), true);
$tracks = array_map(function ($track) {
    unset($track['id']);
    return $track;
}, $tracks);
// Insert all data.
$trackStore->insertMany($tracks);

echo "Seeded";

Példa – tracklista

Plates: Natív PHP sablonok

<?php
require __DIR__ . '/vendor/autoload.php';

// Data
$dataDir = "./data";
$trackStore = \SleekDB\SleekDB::store('tracks', $dataDir);
$tracks = $trackStore->fetch();

// Template
$templates = new League\Plates\Engine('./templates');
echo $templates->render('track_list', ['tracks' => $tracks]);

Példa – tracklista

Sablon és layout

<?php $this->layout('layouts/default') ?>
<ul>
    <?php foreach($tracks as $track) : ?>
    <li style="background-color: <?= $track['color'] ?>">
        <span>🎵 <?= $track['category'] ?></span>
        <?= $track['name'] ?> (<?= $track['instrument'] ?>)
    </li>
    <?php endforeach ?>
</ul>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MIDI editor</title>
</head>
<body>
    <?=$this->section('content')?>
</body>
</html>

Példa – űrlap

<?php

function validate($post, &$data, &$errors) {
  if (!(isset($post['name']) && trim($post['name'])!=='')) {
    $errors['name'] = "The track name is required";
  } else {
    $data['name'] = $post['name'];
  }

  if (!(isset($post['color']) && trim($post['color'])!=='')) {
    $errors['color'] = "The track color is required";
  } 
  else if (filter_var($post['color'], FILTER_VALIDATE_REGEXP, [
    "options"=>[
      "regexp"=>"/^#[0-9a-f]{6}$/",
    ],
  ]) === false) {
    $errors['color'] = "The track color has a wrong format";
  }
  else {
    $data['color'] = $post['color'];
  }

  if (!(isset($post['category']) && trim($post['category'])!=='')) {
    $errors['category'] = "The category is required";
  } else {
    $data['category'] = $post['category'];
  }

  if (!(isset($post['instrument']) && trim($post['instrument'])!=='')) {
    $errors['instrument'] = "The instrument is required";
  }
  else if (filter_var($post['instrument'], FILTER_VALIDATE_INT) === false) {
    $errors['instrument'] = "The instrument has to be an integer";
  } else {
    $data['instrument'] = $post['instrument'];
  }
  
  return count($errors) === 0;
}

function add_track($data) {
  $data["id"] = uniqid();
  $data["notes"] = [];

  $tracks = json_decode(file_get_contents('tracks.array.json'), true);
  $tracks[] = $data;
  file_put_contents('tracks.array.json', json_encode($tracks), LOCK_EX);
}

$data = [];
$errors = [];
if ($_POST) {
  if (validate($_POST, $data, $errors)) {
    add_track($data);
    header('Location: index.php');
    exit();
  }
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>MIDI editor - Add new track</title>
  <link rel="stylesheet" href="https://webprogramozas.inf.elte.hu/webprog/zh/midi/midi.css">
</head>

<body>
  <h2>Add new track</h2>
  <form action="" method="post">
    <?php if ($errors) : ?>
      <div class="errors">
        <?= var_dump($errors) ?>
      </div>
    <?php endif ?>
    <div>
      <label for="name">Track name</label>
      <input type="text" id="name" name="name">
      (required)
    </div>
    <div>
      <label for="color">Color</label>
      <input type="text" id="color" name="color" placeholder="#1234af">
      (required, format: hex color code, e.g. #12af4d)
    </div>
    <div>
      <label for="category">Category</label>
      <input type="text" id="category" name="category" list="category-list">
      (required)
      <datalist id="category-list">
        <option value="Piano">
        <option value="Organ">
        <option value="Accordion">
        <option value="Strings">
        <option value="Guitar">
        <option value="Bass">
        <option value="Choir">
        <option value="Trumpet">
        <option value="Brass">
        <option value="Saxophone">
        <option value="Flute">
        <option value="Synth Lead">
        <option value="Synth Pad">
        <option value="Percussion">
        <option value="World">
        <option value="Synth effects">
        <option value="Sound effects">
      </datalist>
    </div>
    <div>
      <label for="instrument">Instrument</label>
      <select id="instrument" name="instrument">
        <option value="100">Instrument 1</option>
        <option value="200">Instrument 2</option>
        <option value="300">Instrument 3</option>
        <option value="400">Instrument 4</option>
        <option value="500">Instrument 5</option>
      </select>
      (required, number)
    </div>
    <div>
      <button type="submit">Add new track</button>
    </div>
  </form>
  <a href="index.php">Return to editor</a>
</body>

</html>

Példa – űrlap

SleekDB

require __DIR__ . '/vendor/autoload.php';

$dataDir = "./data";
$trackStore = \SleekDB\SleekDB::store('tracks', $dataDir);

// function validate() {}

function add_track($trackStore, $data) {
  $data["notes"] = [];
  $trackStore->insert($data);
}

$data = [];
$errors = [];
if ($_POST) {
  if (validate($_POST, $data, $errors)) {
    add_track($trackStore, $data);
    header('Location: index.php');
    exit();
  }
}

Példa – űrlap

Rakit: űrlapellenőrzés

require __DIR__ . '/vendor/autoload.php';
use Rakit\Validation\Validator;

$dataDir = "./data";
$trackStore = \SleekDB\SleekDB::store('tracks', $dataDir);

function add_track($trackStore, $data) {
  $data["notes"] = [];
  $trackStore->insert($data);
}

$errors = [];
if ($_POST) {
  $validator = new Validator();
  $validation = $validator->validate($_POST, [
      'name'                 => 'required',
      'color'                => 'required|regex:/^#[0-9a-f]{6}$/',
      'category'             => 'required',
      'instrument'           => 'required|integer',
  ]);
  
  if ($validation->fails()) {
    // handling errors
    $errors = $validation->errors()->all();
  } else {
    // validation passes
    $data = $validation->getValidData();
    add_track($trackStore, $data);
    header('Location: index.php');
    exit();
  }
}

Példa – űrlap

Plates: sablon

<?php
require __DIR__ . '/vendor/autoload.php';
use Rakit\Validation\Validator;

$dataDir = "./data";
$trackStore = \SleekDB\SleekDB::store('tracks', $dataDir);

function add_track($trackStore, $data) {
  $data["notes"] = [];
  $trackStore->insert($data);
}

$errors = [];
if ($_POST) {
  $validator = new Validator();
  $validation = $validator->validate($_POST, [
      'name'                 => 'required',
      'color'                => 'required|regex:/^#[0-9a-f]{6}$/',
      'category'             => 'required',
      'instrument'           => 'required|integer',
  ]);
  
  if ($validation->fails()) {
    // handling errors
    $errors = $validation->errors()->all();
  } else {
    // validation passes
    $data = $validation->getValidData();
    add_track($trackStore, $data);
    header('Location: index.php');
    exit();
  }
}

// Template
$templates = new League\Plates\Engine('./templates');
echo $templates->render('new_track', ['errors' => $errors]);

PSR, SPL

Környezet beállítása

Lehetőségek

  • távoli szerver
  • teljes lokális környezet (pl. XAMPP, WAMP)
  • konténerizált lokális környezet
  • minimál lokális környezet (ajánlott telepítő)
    • php
    • composer
    • php -S localhost:3000

Végszó

  • Függvénykönyvtárak használata
  • Composer