Skip to content

Commit b7834d8

Browse files
markcclaude
andcommitted
Refactor 06-Session to app shell with enhanced session demos
- Remove Themes/ folder, use single app shell layout - Add sticky plugin parameter (o persists, m resets to list) - Add clean URLs via history.replaceState() after page load - Add session-backed draft saving in Contact form - Add session reset and ID regeneration demos - Add flash message system with toast display - Add live session info (ID, name, size, keys, visit count) - Add Util::formatBytes() helper Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5cbff8b commit b7834d8

File tree

15 files changed

+244
-332
lines changed

15 files changed

+244
-332
lines changed

06-Session/src/Core/Ctx.php

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
11
<?php declare(strict_types=1);
2-
32
// Copyright (C) 2015-2026 Mark Constable <mc@netserva.org> (MIT License)
43

54
namespace SPE\Session\Core;
65

76
final class Ctx
87
{
98
public array $in;
10-
public array $out;
119

1210
public function __construct(
1311
public string $email = 'mc@netserva.org',
14-
array $in = ['o' => 'Home', 'm' => 'list', 't' => 'Simple', 'x' => ''],
15-
array $out = ['doc' => 'SPE::06', 'head' => '', 'main' => '', 'foot' => ''],
16-
public array $nav = [
17-
['home', 'Home', 'Home'],
18-
['book-open', 'About', 'About'],
19-
['mail', 'Contact', 'Contact'],
20-
],
21-
public array $themes = [
22-
['layout-template', 'Simple', 'Simple'],
23-
['navigation', 'TopNav', 'TopNav'],
24-
['panel-left', 'SideBar', 'SideBar'],
25-
],
12+
array $in = ['o' => 'Home', 'm' => 'list', 'x' => ''],
13+
public array $out = ['doc' => 'SPE::06', 'page' => '← 06 Session', 'head' => '', 'main' => '', 'foot' => ''],
14+
public array $nav = [['home', 'Home', 'Home'], ['book-open', 'About', 'About'], ['mail', 'Contact', 'Contact']],
15+
public array $colors = [['circle', 'Stone', 'default'], ['waves', 'Ocean', 'ocean'], ['trees', 'Forest', 'forest'], ['sunset', 'Sunset', 'sunset']],
2616
) {
2717
session_status() === PHP_SESSION_NONE && session_start();
28-
29-
// Sticky parameters: URL overrides session, session persists across requests
30-
$this->in = array_map($this->ses(...), array_keys($in), $in)
31-
|> (static fn($v) => array_combine(array_keys($in), $v));
32-
$this->out = $out;
18+
// Only 'o' (plugin) is sticky; 'm' (method) defaults to 'list' each request
19+
$this->in = [
20+
'o' => $this->ses('o', $in['o']),
21+
'm' => ($_REQUEST['m'] ?? $in['m']) |> trim(...) |> htmlspecialchars(...),
22+
'x' => ($_REQUEST['x'] ?? $in['x']) |> trim(...) |> htmlspecialchars(...),
23+
];
3324
}
3425

3526
// Get/set session value: URL param overrides, else use session, else use default

06-Session/src/Core/Init.php

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,28 @@
11
<?php declare(strict_types=1);
2-
32
// Copyright (C) 2015-2026 Mark Constable <mc@netserva.org> (MIT License)
43

54
namespace SPE\Session\Core;
65

76
final readonly class Init
87
{
98
private const string NS = 'SPE\\Session\\';
10-
119
private array $out;
1210

13-
public function __construct(
14-
private Ctx $ctx,
15-
) {
16-
[$o, $m, $t] = [$ctx->in['o'], $ctx->in['m'], $ctx->in['t']];
17-
11+
public function __construct(private Ctx $ctx)
12+
{
13+
[$o, $m] = [$ctx->in['o'], $ctx->in['m']];
1814
$model = self::NS . "Plugins\\{$o}\\{$o}Model";
1915
$ary = class_exists($model) ? new $model($ctx)->$m() : [];
20-
2116
$view = self::NS . "Plugins\\{$o}\\{$o}View";
22-
$main = class_exists($view) ? new $view($ctx, $ary)->$m() : "<p>{$ary['main']}</p>";
23-
17+
$main = class_exists($view) ? new $view($ctx, $ary)->$m() : new View($ctx, $ary)->$m();
2418
$this->out = [...$ctx->out, ...$ary, 'main' => $main];
2519
}
2620

2721
public function __toString(): string
2822
{
29-
$t = $this->ctx->in['t'];
30-
$theme = self::NS . "Themes\\{$t}";
3123
return match ($this->ctx->in['x']) {
3224
'json' => (header('Content-Type: application/json') ?: '') . json_encode($this->out),
33-
default => new $theme($this->ctx, $this->out)->render(),
25+
default => new Theme($this->ctx, $this->out)->render(),
3426
};
3527
}
3628
}

06-Session/src/Core/Plugin.php

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,10 @@
11
<?php declare(strict_types=1);
2-
32
// Copyright (C) 2015-2026 Mark Constable <mc@netserva.org> (MIT License)
43

54
namespace SPE\Session\Core;
65

76
abstract class Plugin
87
{
9-
public function __construct(
10-
protected Ctx $ctx,
11-
) {}
12-
13-
public function create(): array
14-
{
15-
return ['head' => 'Create', 'main' => 'Not implemented'];
16-
}
17-
18-
public function read(): array
19-
{
20-
return ['head' => 'Read', 'main' => 'Not implemented'];
21-
}
22-
23-
public function update(): array
24-
{
25-
return ['head' => 'Update', 'main' => 'Not implemented'];
26-
}
27-
28-
public function delete(): array
29-
{
30-
return ['head' => 'Delete', 'main' => 'Not implemented'];
31-
}
32-
33-
public function list(): array
34-
{
35-
return ['head' => 'List', 'main' => 'Not implemented'];
36-
}
8+
public function __construct(protected Ctx $ctx) {}
9+
public function list(): array { return ['head' => 'List', 'main' => 'Not implemented']; }
3710
}

06-Session/src/Core/Theme.php

Lines changed: 58 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,86 @@
11
<?php declare(strict_types=1);
2-
32
// Copyright (C) 2015-2026 Mark Constable <mc@netserva.org> (MIT License)
43

54
namespace SPE\Session\Core;
65

7-
abstract class Theme
6+
final class Theme
87
{
9-
public function __construct(
10-
protected Ctx $ctx,
11-
protected array $out,
12-
) {}
8+
public function __construct(private Ctx $ctx, private array $out) {}
139

14-
abstract public function render(): string;
10+
public function render(): string
11+
{
12+
return $this->html($this->topnav() . $this->sidebar('left') . $this->sidebar('right') . "<main>{$this->out['main']}</main>");
13+
}
1514

16-
protected function nav(): string
15+
private function navLinks(): string
1716
{
18-
['o' => $o, 't' => $t] = $this->ctx->in;
19-
return $this->ctx->nav
20-
|> (static fn($n) => array_map(static fn($p) => sprintf(
21-
'<a href="?o=%s"%s><i data-lucide="%s"></i> %s</a>',
22-
$p[2],
23-
$o === $p[2] ? ' class="active"' : '',
24-
$p[0],
25-
$p[1],
26-
), $n))
27-
|> (static fn($a) => implode(' ', $a));
17+
return implode('', array_map(fn($p) => sprintf(
18+
'<a href="?o=%s"%s data-icon="%s"><i data-lucide="%s"></i> %s</a>',
19+
$p[2], $this->ctx->in['o'] === $p[2] ? ' class="active"' : '', $p[0], $p[0], $p[1]
20+
), $this->ctx->nav));
2821
}
2922

30-
protected function dropdown(): string
23+
private function colorLinks(): string
3124
{
32-
['o' => $o, 't' => $t] = $this->ctx->in;
33-
$links = $this->ctx->themes
34-
|> (static fn($n) => array_map(static fn($p) => sprintf(
35-
'<a href="?t=%s"%s><i data-lucide="%s"></i> %s</a>',
36-
$p[2],
37-
$t === $p[2] ? ' class="active"' : '',
38-
$p[0],
39-
$p[1],
40-
), $n))
41-
|> (static fn($a) => implode('', $a));
42-
return "<div class=\"dropdown\"><span class=\"dropdown-toggle\"><i data-lucide=\"layout-grid\"></i> Layout</span><div class=\"dropdown-menu\">$links</div></div>";
25+
return implode('', array_map(fn($p) => sprintf(
26+
'<a href="#" data-scheme="%s" data-icon="%s"><i data-lucide="%s"></i> %s</a>', $p[2], $p[0], $p[0], $p[1]
27+
), $this->ctx->colors));
28+
}
29+
30+
private function topnav(): string
31+
{
32+
return <<<HTML
33+
<nav class="topnav">
34+
<button class="menu-toggle" data-sidebar="left"><i data-lucide="menu"></i></button>
35+
<h1><a class="brand" href="/"><span>{$this->out['page']}</span></a></h1>
36+
<button class="menu-toggle" data-sidebar="right"><i data-lucide="menu"></i></button>
37+
</nav>
38+
HTML;
4339
}
4440

45-
protected function colors(): string
41+
private function sidebar(string $side): string
4642
{
43+
[$nav, $title, $icon] = $side === 'left'
44+
? [$this->navLinks(), 'Navigation', 'compass']
45+
: [$this->colorLinks() . '<div class="sidebar-divider"></div><a href="#" onclick="Base.toggleTheme();return false" data-icon="moon"><i data-lucide="moon"></i> Toggle Theme</a>', 'Settings', 'sliders-horizontal'];
4746
return <<<HTML
48-
<div class="dropdown"><span class="dropdown-toggle"><i data-lucide="swatch-book"></i> Colors</span><div class="dropdown-menu">
49-
<a href="#" data-scheme="default"><i data-lucide="circle"></i> Stone</a>
50-
<a href="#" data-scheme="ocean"><i data-lucide="waves"></i> Ocean</a>
51-
<a href="#" data-scheme="forest"><i data-lucide="trees"></i> Forest</a>
52-
<a href="#" data-scheme="sunset"><i data-lucide="sunset"></i> Sunset</a>
53-
</div></div>
54-
HTML;
47+
<aside class="sidebar sidebar-{$side}">
48+
<div class="sidebar-header"><span><i data-lucide="{$icon}"></i> {$title}</span><button class="pin-toggle" data-sidebar="{$side}" title="Pin sidebar"><i data-lucide="pin"></i></button></div>
49+
<nav>{$nav}</nav>
50+
</aside>
51+
HTML;
5552
}
5653

57-
protected function flash(): string
54+
private function flash(): string
5855
{
5956
$msg = $this->ctx->flash('msg');
6057
$type = $this->ctx->flash('type') ?? 'success';
61-
return $msg ? "<script>showToast('$msg', '$type');</script>" : '';
58+
return $msg ? "<script>showToast('{$msg}', '{$type}');</script>" : '';
6259
}
6360

64-
protected function html(string $theme, string $body): string
61+
private function html(string $body): string
6562
{
6663
$flash = $this->flash();
6764
return <<<HTML
68-
<!DOCTYPE html>
69-
<html lang="en">
70-
<head>
71-
<meta charset="utf-8">
72-
<meta name="viewport" content="width=device-width, initial-scale=1">
73-
<title>{$this->out['doc']} [$theme]</title>
74-
<link rel="stylesheet" href="/base.css">
75-
<link rel="stylesheet" href="/site.css">
76-
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
77-
<script>(function(){const t=localStorage.getItem("base-theme"),s=localStorage.getItem("base-scheme"),c=t||(matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light");document.documentElement.className=c+(s&&s!=="default"?" scheme-"+s:"")})();</script>
78-
</head>
79-
<body>
80-
$body
81-
<script src="/base.js"></script>
82-
$flash
83-
</body>
84-
</html>
85-
HTML;
65+
<!DOCTYPE html>
66+
<html lang="en">
67+
<head>
68+
<meta charset="utf-8">
69+
<meta name="viewport" content="width=device-width, initial-scale=1">
70+
<title>{$this->out['doc']}</title>
71+
<link rel="stylesheet" href="/base.css">
72+
<link rel="stylesheet" href="/site.css">
73+
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
74+
<script>(function(){var s=JSON.parse(localStorage.getItem('base-state')||'{}'),t=s.theme,c=s.scheme,h=document.documentElement;h.className='preload '+(t||(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'))+(c&&c!=='default'?' scheme-'+c:'');})()</script>
75+
</head>
76+
<body>
77+
{$body}
78+
<div class="overlay"></div>
79+
<script src="/base.js"></script>
80+
<script>if(location.search)history.replaceState(null,'',location.pathname);</script>
81+
{$flash}
82+
</body>
83+
</html>
84+
HTML;
8685
}
8786
}

06-Session/src/Core/Util.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<?php declare(strict_types=1);
2-
32
// Copyright (C) 2015-2026 Mark Constable <mc@netserva.org> (MIT License)
43

54
namespace SPE\Session\Core;
@@ -9,17 +8,23 @@ final class Util
98
public static function timeAgo(int $ts): string
109
{
1110
$d = time() - $ts;
12-
if ($d < 10)
13-
return 'just now';
14-
$units = [['hour', 3600], ['min', 60], ['sec', 1]];
15-
foreach ($units as [$name, $secs]) {
16-
if ($d < $secs) {
17-
continue;
11+
if ($d < 10) return 'just now';
12+
foreach ([['hour', 3600], ['min', 60], ['sec', 1]] as [$name, $secs]) {
13+
if ($d >= $secs) {
14+
$amt = (int) ($d / $secs);
15+
return "{$amt} {$name}" . ($amt > 1 ? 's' : '') . ' ago';
1816
}
19-
20-
$amt = (int) ($d / $secs);
21-
return "$amt $name" . ($amt > 1 ? 's' : '') . ' ago';
2217
}
2318
return 'just now';
2419
}
20+
21+
public static function formatBytes(int $bytes): string
22+
{
23+
if ($bytes < 1024) return "{$bytes} B";
24+
foreach (['KB', 'MB', 'GB'] as $unit) {
25+
$bytes /= 1024;
26+
if ($bytes < 1024) return round($bytes, 1) . " {$unit}";
27+
}
28+
return round($bytes, 1) . ' TB';
29+
}
2530
}

06-Session/src/Core/View.php

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
11
<?php declare(strict_types=1);
2-
32
// Copyright (C) 2015-2026 Mark Constable <mc@netserva.org> (MIT License)
43

54
namespace SPE\Session\Core;
65

76
class View
87
{
9-
public function __construct(
10-
protected Ctx $ctx,
11-
protected array $ary,
12-
) {}
13-
14-
public function list(): string
15-
{
16-
return <<<HTML
17-
<div class="card">
18-
<h2>{$this->ary['head']}</h2>
19-
<p>{$this->ary['main']}</p>
20-
</div>
21-
HTML;
22-
}
8+
public function __construct(protected Ctx $ctx, protected array $ary) {}
9+
public function list(): string { return "<div class=\"card\"><h2>{$this->ary['head']}</h2><p>{$this->ary['main']}</p></div>"; }
2310
}

06-Session/src/Plugins/About/AboutModel.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<?php declare(strict_types=1);
2-
32
// Copyright (C) 2015-2026 Mark Constable <mc@netserva.org> (MIT License)
43

54
namespace SPE\Session\Plugins\About;
@@ -11,9 +10,12 @@ final class AboutModel extends Plugin
1110
#[\Override]
1211
public function list(): array
1312
{
13+
$_SESSION['visit_count'] = ($_SESSION['visit_count'] ?? 0) + 1;
14+
$_SESSION['last_page'] = 'About';
15+
1416
return [
1517
'head' => 'About Page',
16-
'main' => 'This chapter adds <b>PHP session management</b> with sticky URL parameters, flash messages, and visit tracking.',
18+
'main' => 'This chapter adds <b>PHP session management</b> for persistent state across requests.',
1719
];
1820
}
1921
}

06-Session/src/Plugins/About/AboutView.php

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)