Compare commits

...

15 Commits

40 changed files with 1755 additions and 771 deletions

View File

@ -1,3 +1,5 @@
## Release 0.8.0p82 du 07/11/2025-10:41
## Release 0.8.0p74 du 07/11/2025-10:39
* `65fbe88` début migration ldap

View File

@ -64,6 +64,8 @@ git checkout dev74
git cherry-pick "$commit"
pp -a
_merge82
## console 82
pu

View File

@ -1,7 +0,0 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use cli\Csv2xlsxApp;
Csv2xlsxApp::run();

View File

@ -1,23 +0,0 @@
<?php
namespace cli;
use nulib\app\cli\Application;
use nulib\cv;
use nulib\ext\tab\SsBuilder;
use nulib\ext\tab\SsReader;
use nulib\file;
use nulib\os\path;
class Csv2xlsxApp extends Application {
function main() {
$input = cv::not_null($this->args[0] ?? null, "input");
$inputname = path::filename($input);
$output = path::ensure_ext($inputname, ".xlsx", ".csv");
$reader = SsReader::with($input);
$builder = SsBuilder::with($output);
$builder->writeAll($reader);
$builder->build();
$builder->copyTo(file::writer($output), true);
}
}

View File

@ -3,6 +3,18 @@
"type": "library",
"description": "espace de maturation pour les librairies",
"repositories": [
{
"type": "path",
"url": "../nulib-base"
},
{
"type": "path",
"url": "../nulib-spout"
},
{
"type": "path",
"url": "../nulib-phpss"
},
{
"type": "composer",
"url": "https://repos.univ-reunion.fr/composer"
@ -18,9 +30,9 @@
"php": "^7.4"
},
"require-dev": {
"nulib/base": "^0.8.0p74",
"nulib/spout": "^0.8.0p74",
"nulib/phpss": "^0.8.0p74",
"nulib/base": "^7.4-dev",
"nulib/spout": "^7.4-dev",
"nulib/phpss": "^7.4-dev",
"nulib/tests": "^7.4",
"ext-posix": "*",
"ext-pcntl": "*",
@ -67,7 +79,6 @@
}
},
"bin": [
"bin/csv2xlsx.php",
"nur_bin/compctl.php",
"nur_bin/compdep.php",
"nur_bin/datectl.php",

73
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b80f028afa573b48945cac46de66b402",
"content-hash": "2dc79f259fa01ba2a990fbcb8fcec728",
"packages": [],
"packages-dev": [
{
@ -997,11 +997,11 @@
},
{
"name": "nulib/base",
"version": "0.8.0p74",
"source": {
"type": "git",
"url": "https://git.univ-reunion.fr/sda-php/nulib-base.git",
"reference": "d14dcd25ee0157d4ec619cd2265b1ad6b2b35a57"
"version": "dev-dev74",
"dist": {
"type": "path",
"url": "../nulib-base",
"reference": "7549840ac9ef5cac0f3b40d9577114bf128f87ec"
},
"require": {
"ext-json": "*",
@ -1036,7 +1036,7 @@
"php/bin/sqlite.dump.php",
"php/bin/mysql.capacitor.php",
"php/bin/pgsql.capacitor.php",
"php/bin/create-capacitor-channels.php"
"php/bin/tabconvert.php"
],
"type": "library",
"extra": {
@ -1063,18 +1063,20 @@
}
],
"description": "fonctions et classes essentielles",
"time": "2025-11-07T06:27:14+00:00"
"transport-options": {
"relative": true
}
},
{
"name": "nulib/phpss",
"version": "0.8.0p74",
"source": {
"type": "git",
"url": "https://git.univ-reunion.fr/sda-php/nulib-phpss.git",
"reference": "053e37d46ab818b619bfa43283c7357a24371cf0"
"version": "dev-dev74",
"dist": {
"type": "path",
"url": "../nulib-phpss",
"reference": "479ca94653cd9a658858342c28b8f3ee912e891b"
},
"require": {
"nulib/base": "^0.8.0p74",
"nulib/base": "^7.4-dev",
"php": "^7.4",
"phpoffice/phpspreadsheet": "^1.0"
},
@ -1105,15 +1107,17 @@
}
],
"description": "wrapper pour phpoffice/phpspreadsheet",
"time": "2025-11-07T06:36:00+00:00"
"transport-options": {
"relative": true
}
},
{
"name": "nulib/spout",
"version": "0.8.0p74",
"source": {
"type": "git",
"url": "https://git.univ-reunion.fr/sda-php/nulib-spout.git",
"reference": "6270fec936bacf5f43fc29d900b3c8702ab61344"
"version": "dev-dev74",
"dist": {
"type": "path",
"url": "../nulib-spout",
"reference": "92734dc7e31f6c499349b7b98e492c7d7a5516f4"
},
"require": {
"ext-dom": "*",
@ -1121,7 +1125,7 @@
"ext-libxml": "*",
"ext-xmlreader": "*",
"ext-zip": "*",
"nulib/base": "^0.8.0p74",
"nulib/base": "^7.4-dev",
"php": "^7.4"
},
"replace": {
@ -1145,7 +1149,10 @@
"psr-4": {
"nulib\\": "src",
"OpenSpout\\": "openspout3/src"
}
},
"files": [
"autoload.php"
]
},
"autoload-dev": {
"psr-4": {
@ -1159,7 +1166,9 @@
}
],
"description": "wrapper pour openspout/openspout",
"time": "2025-11-07T06:32:08+00:00"
"transport-options": {
"relative": true
}
},
{
"name": "nulib/tests",
@ -4224,16 +4233,16 @@
},
{
"name": "theseer/tokenizer",
"version": "1.2.3",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
@ -4262,7 +4271,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.3"
"source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
@ -4270,12 +4279,16 @@
"type": "github"
}
],
"time": "2024-03-03T12:36:25+00:00"
"time": "2025-11-17T20:03:58+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"nulib/base": 20,
"nulib/spout": 20,
"nulib/phpss": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View File

@ -20,6 +20,7 @@ use nur\v\page;
use nur\v\prefix;
use nur\v\vo;
use Throwable;
use nulib\app\config as nconfig;
abstract class AbstractPageContainer implements IPageContainer {
protected static function ensure_preparec(IComponent $c, bool $afterPrepare=false): bool {
@ -265,6 +266,7 @@ abstract class AbstractPageContainer implements IPageContainer {
$page->afterConfig();
}
config::configure($this->config["configure_options"]);
nconfig::configure($this->config["configure_options"]);
$this->phase = self::SETUP_PHASE;
if (self::ensure_setupc($page, false, $output)) {

View File

@ -1,11 +1,13 @@
<?php
namespace nur\v;
use nulib\app\app;
use nur\A;
use nur\config;
use nur\v\html5\Html5NavigablePageContainer;
use nur\v\model\IPage;
use nur\v\model\IPageContainer;
use nulib\app\config as nconfig;
class page {
const CONTAINER_CLASS = Html5NavigablePageContainer::class;
@ -36,11 +38,14 @@ class page {
}
static final function render(?IPage $page=null): void {
app::set_fact(app::FACT_WEB_APP);
config::set_fact(config::FACT_WEB_APP);
config::configure(config::CONFIGURE_INITIAL_ONLY);
nconfig::configure(nconfig::CONFIGURE_INITIAL_ONLY);
$pc = self::container();
if ($page === null) {
config::configure(config::CONFIGURE_ROUTES_ONLY);
nconfig::configure(nconfig::CONFIGURE_ROUTES_ONLY);
$page = route::get_page();
}
$pc->setPage($page);

View File

@ -261,7 +261,7 @@ class v {
return $pieces;
}
private static function _list(?iterable $values, ?string $sep=", ", ?string $prefix=null, ?string $suffix=null, ?callable $function): array {
private static function _list(?iterable $values, ?string $sep=", ", ?string $prefix=null, ?string $suffix=null, ?callable $function=null): array {
$vs = [];
if ($values !== null) {
$functx = $function !== null? func::_prepare($function): null;
@ -285,7 +285,7 @@ class v {
}
static final function simple_list(?iterable $values, ?string $sep=", ", ?string $prefix=null, ?string $suffix=null): array {
return self::_list($values, $sep, $prefix, $suffix, null);
return self::_list($values, $sep, $prefix, $suffix);
}
const LIST_SCHEMA = [

236
src/app/web/Application.php Normal file
View File

@ -0,0 +1,236 @@
<?php
namespace nulib\app\web;
use Exception;
use nulib\app\app;
use nulib\app\config;
use nulib\exceptions;
use nulib\ExitError;
use nulib\output\con;
use nulib\output\log;
use nulib\output\msg;
use nulib\output\std\LogMessenger;
use nulib\php\func;
use nulib\web\base\Html5PageRenderer;
use nulib\web\base\JsonRenderer;
use nulib\web\base\TextRenderer;
use nulib\web\model\IComponent;
use nulib\web\model\IPage;
use nulib\web\page;
/**
* Class Application: application de base
*/
abstract class Application {
/** @var string répertoire du projet (celui qui contient composer.json */
const PROJDIR = null;
/**
* @var array répertoires vendor exprimés relativement à PROJDIR
*
* les clés suivantes doivent être présentes dans le tableau:
* - autoload (chemin vers vendor/autoload.php)
* - bindir (chemin vers vendor/bin)
*/
const VENDOR = null;
/**
* @var string code du projet, utilisé pour dériver le noms de certains des
* paramètres extraits de l'environnement, e.g MY_APP_DATADIR si le projet a
* pour code my-app
*
* si non définie, cette valeur est calculée automatiquement à partir de
* self::PROJDIR sans le suffixe "-app"
*/
const PROJCODE = null;
/**
* @var string|null identifiant d'un groupe auquel l'application appartient.
* les applications du même groupe enregistrent leur fichiers de controle au
* même endroit $VARDIR/$APPGROUP
*/
const APPGROUP = null;
/**
* @var string code de l'application, utilisé pour inférer le nom de certains
* fichiers spécifiques à l'application.
*
* si non définie, cette valeur est calculée automatiquement à partir de
* static::class
*/
const NAME = null;
/** @var string description courte de l'application */
const TITLE = null;
const DATADIR = null;
const ETCDIR = null;
const VARDIR = null;
const CACHEDIR = null;
const LOGDIR = null;
const MODE_EXACT = RouteManager::MODE_EXACT;
const MODE_PREFIX = RouteManager::MODE_PREFIX;
const MODE_PACKAGE = RouteManager::MODE_PACKAGE;
const MODE_PACKAGE2 = RouteManager::MODE_PACKAGE2;
const ROUTES = [
];
const PAGE = null;
static function run($page=null, ?Application $app=null): void {
try {
static::_initialize_app();
$app ??= new static();
page::set_app($app);
$app->render($page ?? static::PAGE);
} catch (ExitError $e) {
if ($e->haveUserMessage()) log::error($e->getUserMessage());
exit($e->getCode());
} catch (Exception $e) {
log::error($e);
exit(app::EC_UNEXPECTED);
}
}
protected static function _initialize_app(): void {
app::init(static::class);
app::set_fact(app::FACT_WEB_APP);
$projcode = static::PROJCODE ?? "application";
$log = new LogMessenger([
"output" => "/tmp/$projcode.out",
"min_level" => msg::DEBUG,
]);
con::set_messenger($log);
msg::set_messenger($log);
}
/**
* sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e
* pas d'erreur)
*
* équivalent à lancer l'exception {@link ExitError}
*/
protected static final function exit(int $exitcode=0, $message=null) {
throw new ExitError($exitcode, $message);
}
/**
* sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e
* une erreur s'est produite)
*
* équivalent à lancer l'exception {@link ExitError}
*/
protected static final function die($message=null, int $exitcode=1) {
throw new ExitError($exitcode, $message);
}
protected RouteManager $routeManager;
function getRouteManager(): RouteManager {
return $this->routeManager;
}
protected function configureRoutes(): void {
$this->routeManager->addRoute(...static::ROUTES);
}
protected function configureLogs(): void {
$log = log::set_messenger(new LogMessenger([
"output" => app::get()->getLogfile(),
"min_level" => msg::MINOR,
]));
msg::set_messenger($log, true);
}
protected function configureNoSession(): void {
config::configure(config::CONFIGURE_NO_SESSION);
}
protected function configureSession(): void {
config::configure();
}
protected function resolvePage($page): IPage {
if ($page instanceof IPage) return $page;
$page ??= $this->routeManager->getPage();
if (is_string($page)) $page = new $page();
elseif (is_array($page)) $page = func::with($page)->invoke();
else throw exceptions::invalid_type($page, "page", ["string", "array"]);
return $page;
}
protected function ensurePhase(IPage $page, int $phase, bool $after=false, &$output=null) {
if (!$page->didComponentPhase($phase)) {
if ($page->beforeComponentPhase($phase)) {
page::set_component_phase($phase);
$output = $page->doComponentPhase($phase);
if ($after) $page->afterComponentPhase($phase);
return true;
}
}
return false;
}
protected ?array $renderers = null;
protected function getRenderer(IPage $page) {
$class = $page->RENDERER_CLASS() ?? Html5PageRenderer::class;
return $this->renderers[$class] ??= new $class();
}
function render($component) {
config::configure(config::CONFIGURE_INITIAL_ONLY);
$this->routeManager = new RouteManager();
$this->configureRoutes();
$this->configureLogs();
$components = [];
$component = $this->resolvePage($component);
while (true) {
page::set_component($component);
array_unshift($components, $component);
if (!$this->ensurePhase($component, page::PHASE_PREPARE)) break;
$component->afterComponentPhase(page::PHASE_PREPARE);
if ($component->USE_SESSION()) $this->configureSession();
else $this->configureNoSession();
if (!$this->ensurePhase($component, page::PHASE_CONFIGURE)) break;
$component->afterComponentPhase(page::PHASE_CONFIGURE);
if (!$this->ensurePhase($component, page::PHASE_SETUP, false, $output)) break;
$component->afterComponentPhase(page::PHASE_SETUP);
page::set_component_phase(page::PHASE_PRINT);
if ($output instanceof IComponent) {
# composant autonome. recommencer la phase de préparation
} elseif (is_callable($output)) {
# générateur de contenu
$output();
break;
} elseif ($output === false) {
# retour en l'état (contenu déjà généré dans setup)
break;
} elseif ($output !== null) {
# contenu json ou texte
if (is_iterable($output)) $renderer = new JsonRenderer();
else $renderer = new TextRenderer();
page::set_renderer($renderer);
$renderer->render($output);
break;
} else {
# afficher la page normalement
$renderer = $this->getRenderer($component);
page::set_renderer($renderer);
$renderer->render($component);
break;
}
}
foreach ($components as $component) {
if ($component->didComponentPhase(page::PHASE_SETUP)) {
$this->ensurePhase($component, page::PHASE_TEARDOWN, true);
}
}
}
}

View File

@ -0,0 +1,285 @@
<?php
namespace nulib\app\web;
use nulib\cl;
use nulib\cv;
use nulib\exceptions;
use nulib\php\func;
use nulib\php\types\vint;
use nulib\str;
use nulib\web\model\IPage;
class RouteManager {
/** @var int indique que la correspondance du chemin doit être exacte */
const MODE_EXACT = 0;
/**
* @var int indique que la correspondance du chemin se fait sur le préfixe
* par exemple:
* - 'index.php' matche 'index.php' et 'index.php/suffix'
* - 'path/to/' matche 'path/to/index.php' et 'path/to/menu.php'",
* si le chemin ne se termine pas par '/', une correspondance exacte est
* automatiquement rajoutée
* contraiement à {@link MODE_PACKAGE}, quel que soit le chemin matché, c'est
* toujours la même page qui prend en charge le chemin
*/
const MODE_PREFIX = 1;
/**
* @var int indique que la correspondance du chemin se fait sur le préfixe, et
* que la classe associée est calculée automatiquement en cherchant à partir
* d'un package spécifié. par exemple:
* - si on a [path]="prefix/" et [page]="package\\Class"
* - alors 'prefix/index.php' est pris en charge par package\IndexPage
* - et 'prefix/sub/do-it.php' est pris en charge par package\sub\DoItPage
* si le chemin ne se termine pas par '/', une correspondance exacte est
* automatiquement rajoutée
*/
const MODE_PACKAGE = 2;
/**
* @var int comme {@link MODE_PACKAGE} mais en utilisant '--' au lieu de '/'
*/
const MODE_PACKAGE2 = 3;
private const VALID_MODES = [
self::MODE_EXACT,
self::MODE_PREFIX,
self::MODE_PACKAGE,
self::MODE_PACKAGE2,
];
function __construct(?array $routes=null) {
$this->eroutes = [];
$this->proutes = [];
$this->errorPage = null; #XXX
if ($routes !== null) $this->addRoute(...$routes);
}
/** routes pour les chemins exacts. elles ont la priorité sur les préfixes */
protected array $eroutes;
/** routes pour les préfixes de chemin */
protected array $proutes;
private static function get_package(?string $class): ?string {
if ($class === null) return null;
str::del_prefix($class, "\\");
if (($pos = strrpos($class, "\\")) === false) return "";
else return substr($class, 0, $pos);
}
function addRoute(array ...$routes): void {
$eroutes =& $this->eroutes;
$proutes =& $this->proutes;
foreach ($routes as $route) {
$path = cv::not_null($route["path"] ?? null, "route path");
str::del_prefix($path, "/");
$page = cv::not_null($route["page"] ?? null, "route page");
$package = self::get_package($page);
$args = cl::with($route["args"] ?? null);
$mode = vint::with($route["mode"] ?? null);
$aliases = cl::with($route["aliases"] ?? null);
$route = [
"path" => $path,
"page" => $page,
"package" => $package,
"args" => $args,
"mode" => $mode,
"aliases" => $aliases,
];
switch ($mode) {
case self::MODE_EXACT:
# route exacte
$eroutes[$path] = $route;
break;
case self::MODE_PREFIX:
case self::MODE_PACKAGE:
case self::MODE_PACKAGE2:
if ($path && !str::ends_with("/", $path)) {
# chemin ne se terminant pas par '/': faire aussi la correspondance
# exacte
$eroutes[$path] = $route;
}
$proutes[$page] = $route;
break;
default:
throw exceptions::invalid_value($mode, "route mode");
}
# faire les aliases
foreach ($route["aliases"] as $alias) {
str::del_prefix($alias, "/");
$route["path"] = $alias;
$route["mode"] = self::MODE_EXACT;
$eroutes[$alias] = $route;
}
}
}
protected string $errorPage;
/**
* spécifier la page par défaut si aucune route ne correspond
*/
function setErrorPage(string $page): void {
$this->errorPage = $page;
}
function getErrorPage($error): array {
return [$this->errorPage, false, $error];
}
/**
* Calculer $page à partir de $path avec les règles suivantes:
* - supprimer le suffixe '.php'
* - remplacer '/' et '--' par '\\'
* - pour chaque élément, transformer camel-case en CamelCase
* - rajouter le suffixe Page
*
* voici comment est calculée la valeur de départ:
* - si $path se termine par '.php', prendre le chemin SANS le suffixe '.php'
* - si $path est la forme '*.php/SUFFIX', prendre SUFFIX comme valeur de
* départ
*/
private function computePackagePage(?string $path, string $package): string {
str::del_prefix($path, "/");
if (str::ends_with(".php", $path)) {
str::del_suffix($path, ".php");
} elseif (($pos = strpos($path, ".php/")) !== false) {
$path = substr($path, $pos + 5);
}
$path = str_replace("--", "/", $path);
$parts = explode("/", "$path-page");
$last = count($parts) - 1;
for ($i = 0; $i <= $last; $i++) {
$parts[$i] = str::us2camel($parts[$i]);
if ($i == $last) $parts[$i] = str::upper1($parts[$i]);
}
str::add_suffix($package, "\\");
return $package.implode("\\", $parts);
}
/**
* obtenir une class dérivée de {@link IPage} correspondant au chemin spécifié
* e.g '/index.php'
*
* si $path===null, prendre le chemin courant i.e "$php_self.$path_info"
*
* Retourner `[$class, false, ...$args]` qui est directement utilisable par
* {@link func}. Le classe doit être instanciée par l'utilisateur
*/
function getPage($path=null): array {
if ($path === null) $path = $_SERVER["PHP_SELF"];
str::del_prefix($path, "/");
# essayer d'abord des routes exactes
$route = cl::get($this->eroutes, $path);
if ($route !== null) {
return [$route["page"], false, ...$route["args"]];
}
# puis, essayer dans l'ordre les routes basées sur des préfixes
foreach ($this->proutes as $route) {
switch ($route["mode"]) {
case self::MODE_PREFIX:
$prefix = $route["path"];
if ($prefix) str::add_suffix($prefix, "/");
if (str::starts_with($prefix, $path)) {
return [$route["page"], false, ...$route["args"]];
}
break;
}
}
# chemin non trouvé, essayer de calculer à partir de $path
$page = false;
foreach ($this->proutes as $route) {
switch ($route["mode"]) {
case self::MODE_PACKAGE:
case self::MODE_PACKAGE2:
$prefix = $route["path"];
if ($prefix) str::add_suffix($prefix, "/");
if (str::starts_with($prefix, $path)) {
$page = $this->computePackagePage(
str::without_prefix($prefix, $path),
$route["package"]);
if (class_exists($page)) return [$page, false, ...$route["args"]];
}
break;
}
}
# sinon prendre la page par défaut (qui est en fait la page d'erreur)
$errorMessage = "$path: unable to find page class";
if ($page !== false) $errorMessage .= " $page";
return $this->getErrorPage($errorMessage);
}
/**
* Calculer $path à partir de $page avec les règles suivantes:
* - supprimer le suffixe Page
* - pour chaque élément, transformer CamelCase en camel-case
* - remplacer '\\' par '/'
* - rajouter le suffixe '.php'
*
* NB: cette fonction n'est pas la symétrique de {@link computePackagePage()}.
* par exemple:
* - computePackagePage() transforme 'p--do-it.php' en 'p\\DoItPage'
* - computePackagePath() transforme 'p\\DoItPage' en 'p/do-it.php'
* pour les cas particulier, il vaut donc mieux faire des routes exactes
*/
private function computePackagePath(string $prefix, string $page, string $package, string $sep="/"): string {
# préfixe
str::add_suffix($prefix, "/");
# classe
if ($package) str::del_prefix($page, "$package\\");
str::del_suffix($page, "Page");
$parts = explode("\\", $page);
$last = count($parts) - 1;
for ($i = 0; $i <= $last; $i++) {
if ($i == $last) $parts[$i] = str::lower1($parts[$i]);
$parts[$i] = str::camel2us($parts[$i], false, "-");
}
$path = implode($sep, $parts);
return "$prefix$path.php";
}
/**
* obtenir le chemin correspondant à la page spécifiée.
*
* @param string|IPage $page
*/
function getPath($page): string {
if ($page instanceof IPage) $page = get_class($page);
if (!is_string($page)) {
throw exceptions::invalid_type($page, "page", ["string", IPage::class]);
}
# d'abord les chemins exacts
foreach ($this->eroutes as $route) {
if ($page === $route["page"]) return $route["path"];
}
# puis les préfixes
foreach ($this->proutes as $route) {
switch ($route["mode"]) {
case self::MODE_PREFIX:
if ($page === $route["page"]) return $route["path"];
break;
}
}
# puis les packages
foreach ($this->proutes as $route) {
$mode = $route["mode"];
switch ($mode) {
case self::MODE_PACKAGE:
case self::MODE_PACKAGE2:
$package = $route["package"];
if (str::starts_with($package, $page)) {
if ($mode == self::MODE_PACKAGE) $sep = "/";
else $sep = "--";
return $this->computePackagePath($route["path"], $page, $package, $sep);
}
break;
}
}
# pas trouvé
throw exceptions::invalid_value($page, "page", "chemin non trouvé");
}
}

View File

@ -1,168 +0,0 @@
<?php # -*- coding: utf-8 mode: php -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
namespace nulib\php;
use Exception;
use Generator;
use Iterator;
use IteratorAggregate;
use nulib\StopException;
use nulib\ValueException;
use Traversable;
/**
* Class iter: gestion des itérateurs
*/
class iter {
private static function unexpected_type($object): ValueException {
return ValueException::invalid_type($object, "iterable");
}
/**
* fermer "proprement" un itérateur ou un générateur. retourner true en cas de
* succès, ou false si c'est un générateur et qu'il ne supporte pas l'arrêt
* avec StopException (la valeur de retour n'est alors pas disponible)
*/
static function close($it): bool {
if ($it instanceof ICloseable) {
$it->close();
return true;
} elseif ($it instanceof Generator) {
try {
$it->throw(new StopException());
return true;
} catch (StopException $e) {
}
}
return false;
}
/**
* retourner la première valeur du tableau, de l'itérateur ou de l'instance
* de Traversable, ou $default si aucun élément n'est trouvé.
*/
static final function first($values, $default=null) {
if ($values instanceof IteratorAggregate) $values = $values->getIterator();
if ($values instanceof Iterator) {
try {
$values->rewind();
$value = $values->valid()? $values->current(): $default;
} finally {
self::close($values);
}
} elseif (is_array($values) || $values instanceof Traversable) {
$value = $default;
foreach ($values as $value) {
break;
}
} else {
throw self::unexpected_type($values);
}
return $value;
}
/**
* retourner la première clé du tableau, de l'itérateur ou de l'instance
* de Traversable, ou $default si aucun élément n'est trouvé.
*/
static final function first_key($values, $default=null) {
if ($values instanceof IteratorAggregate) $values = $values->getIterator();
if ($values instanceof Iterator) {
try {
$values->rewind();
$key = $values->valid()? $values->key(): $default;
} finally {
self::close($values);
}
} elseif (is_array($values) || $values instanceof Traversable) {
$key = $default;
foreach ($values as $key => $ignored) {
break;
}
} else {
throw self::unexpected_type($values);
}
return $key;
}
#############################################################################
# outils pour gérer de façon générique des instances de {@link Iterator} ou
# des arrays
/**
* @param $it ?iterable|array
* @return bool true si l'itérateur ou le tableau ont pu être réinitialisés
*/
static function rewind(&$it, ?Exception &$exception=null): bool {
if ($it instanceof Iterator) {
try {
$exception = null;
$it->rewind();
return true;
} catch (Exception $e) {
$exception = $e;
}
} elseif ($it !== null) {
reset($it);
return true;
}
return false;
}
/**
* @param $it ?iterable|array
*/
static function valid($it): bool {
if ($it instanceof Iterator) {
return $it->valid();
} elseif ($it !== null) {
return key($it) !== null;
} else {
return false;
}
}
/**
* @param $it ?iterable|array
*/
static function current($it, &$key=null) {
if ($it instanceof Iterator) {
$key = $it->key();
return $it->current();
} elseif ($it !== null) {
$key = key($it);
return current($it);
} else {
$key = null;
return null;
}
}
/**
* @param $it ?iterable|array
*/
static function next(&$it, ?Exception &$exception=null): void {
if ($it instanceof Iterator) {
try {
$exception = null;
$it->next();
} catch (Exception $e) {
$exception = $e;
}
} elseif ($it !== null) {
next($it);
}
}
/**
* obtenir la valeur de retour si $it est un générateur terminé, ou null sinon
*/
static function get_return($it) {
if ($it instanceof Generator) {
try {
return $it->getReturn();
} catch (Exception $e) {
}
}
return null;
}
}

View File

@ -1,310 +0,0 @@
<?php
namespace nulib\php\iter;
use ArrayAccess;
use Iterator;
use IteratorAggregate;
use nulib\cl;
use nulib\php\func;
use nulib\php\iter;
use Traversable;
/**
* Class Cursor: parcours des lignes itérable
*
* XXX si on spécifie $cols ou $colsFunc, il y a une possibilité que les clés de
* $row ne soient pas dans le bon ordre, ou que les clés de $cols ne soient pas
* présentes dans $row. ajouter les paramètres ensure_keys et order_keys
*
* @property-read array|null $value alias pour $row
* @property-read iterable|null $rows la source des lignes
*/
class Cursor implements Iterator, ArrayAccess {
const PARAMS_SCHEMA = [
"rows" => ["?iterable"],
"rows_func" => ["?callable"],
"filter" => ["?array"],
"filter_func" => ["?callable"],
"map" => ["?array"],
"map_func" => ["?callable"],
"cols" => ["?array"],
"cols_func" => ["?callable"],
];
function __construct(?iterable $rows=null, ?array $params=null) {
if ($rows !== null) $params["rows"] = $rows;
$rows = $params["rows"] ?? null;
$rowsGenerator = null;
$rowsFunc = $params["rows_func"] ?? null;
if ($rowsFunc !== null) {
if ($rowsFunc instanceof Traversable) {
$rowsGenerator = $rowsFunc;
$rowsFunc = null;
} else {
$rowsFunc = func::with($rowsFunc, [$rows, $this]);
}
} elseif ($rows instanceof Traversable) {
$rowsGenerator = $rows;
} else {
$rowsFunc = func::with(function() use ($rows) {
return $rows;
});
}
$this->rowsGenerator = $rowsGenerator;
$this->rowsFunc = $rowsFunc;
$filter = $params["filter"] ?? null;
$filterFunc = $params["filter_func"] ?? null;
if ($filterFunc !== null) $this->setFilterFunc($filterFunc);
elseif ($filter !== null) $this->setFilter($filter);
$map = $params["map"] ?? null;
$mapFunc = $params["map_func"] ?? null;
if ($mapFunc !== null) $this->setMapFunc($mapFunc);
elseif ($map !== null) $this->setMap($map);
$this->cols = $params["cols"] ?? null;
$this->setColsFunc($params["cols_func"] ?? null);
}
/** un générateur de lignes */
private ?Traversable $rowsGenerator;
/**
* une fonction de signature <code>function(mixed $rows, Cursor): ?iterable</code>
*/
private ?func $rowsFunc;
/**
* une fonction de signature <code>function(?array $row, Cursor): bool</code>
*/
private ?func $filterFunc = null;
function setFilter(array $filter): self {
$this->filterFunc = func::with(function(?array $row) use ($filter) {
return cl::filter($row, $filter);
});
return $this;
}
function setFilterFunc(?callable $func): self {
if ($func === null) $this->filterFunc = null;
else $this->filterFunc = func::with($func)->bind($this);
return $this;
}
/**
* une fonction de signature <code>function(?array $row, Cursor): ?array</code>
*/
private ?func $mapFunc = null;
function setMap(array $map): self {
$this->mapFunc = func::with(function(?array $row) use ($map) {
return cl::map($row, $map);
});
return $this;
}
function setMapFunc(?callable $func): self {
if ($func === null) $this->mapFunc = null;
else $this->mapFunc = func::with($func)->bind($this);
return $this;
}
/**
* une fonction de signature <code>function(?array $row, Cursor): ?array</code>
*/
private ?func $colsFunc = null;
function setColsFunc(?callable $func): self {
$this->cols = null;
if ($func === null) $this->colsFunc = null;
else $this->colsFunc = func::with($func)->bind($this);
return $this;
}
/** @var iterable|null source des éléments */
protected ?iterable $rows;
/** @var array|null listes des colonnes de chaque enregistrement */
public ?array $cols;
/**
* @var int index de l'enregistrement (en ne comptant pas les éléments filtrés)
*/
public int $index;
/**
* @var int index original de l'enregistrement (en tenant compte des éléments
* filtrés)
*/
public int $origIndex;
/** @var string|int clé de l'enregistrement */
public $key;
/** @var mixed élément original récupéré depuis la source */
public $raw;
/**
* @var array|null enregistrement après conversion en tableau et application
* du mapping
*/
public ?array $row;
function __get($name) {
if ($name === "value") return $this->row;
elseif ($name == "rows") return $this->rows;
trigger_error("Undefined property $name");
return null;
}
function offsetExists($offset): bool {
return cl::has($this->row, $offset);
}
function offsetGet($offset) {
return cl::get($this->row, $offset);
}
function offsetSet($offset, $value): void {
cl::set($this->row, $offset, $value);
}
function offsetUnset($offset): void {
cl::del($this->row, $offset);
}
/**
* données de session: cela permet de maintenir certaines informations pendant
* le parcours des données
*/
protected ?array $data;
/** @param string|int $key */
function has($key): bool {
return cl::has($this->data, $key);
}
/** @param string|int $key */
function get($key) {
return cl::get($this->data, $key);
}
/** @param string|int $key */
function set($key, $value): void {
$this->data[$key] = $value;
}
/** @param string|int $key */
function del($key) {
$orig = cl::get($this->data, $key);
unset($this->data[$key]);
return $orig;
}
protected function convertToRow($raw): ?array {
return cl::withn($raw);
}
protected function filterFunc(?array $row): bool {
return false;
}
protected function mapFunc(?array $row): ?array {
return $this->row;
}
protected function colsFunc(?array $row): ?array {
return $this->row !== null? array_keys($this->row): null;
}
#############################################################################
# Iterator
function rewind() {
$this->cols = null;
$this->index = 0;
$this->origIndex = 0;
$this->key = null;
$this->raw = null;
$this->row = null;
$this->data = null;
if ($this->rowsGenerator !== null) {
$rows = $this->rowsGenerator;
if ($rows instanceof IteratorAggregate) $rows = $rows->getIterator();
$rows->rewind();
$this->rows = $rows;
} else {
$this->rows = $this->rowsFunc->invoke();
}
}
function valid(): bool {
$colsFunc = $this->colsFunc;
$filterFunc = $this->filterFunc;
$mapFunc = $this->mapFunc;
while ($valid = iter::valid($this->rows)) {
$this->raw = iter::current($this->rows, $this->key);
# conversion en enregistrement
$this->key ??= $this->origIndex;
$this->row = $this->convertToRow($this->raw);
# filtrage
if ($filterFunc === null) $filtered = $this->filterFunc($this->row);
else $filtered = $filterFunc->invoke([$this->row, $this]);
if ($filtered) {
iter::next($this->rows);
$this->origIndex++;
} else {
# l'enregistrement n'as pas été filtré: faire le mapping
if ($mapFunc === null) $this->row = $this->mapFunc($this->row);
else $this->row = $mapFunc->invoke([$this->row, $this]);
# calculer la liste des colonnes le cas échéant
if ($this->cols === null) {
if ($colsFunc === null) $this->cols = $this->colsFunc($this->row);
else $this->cols = $colsFunc->invoke([$this->row, $this]);
}
break;
}
}
if (!$valid) {
iter::close($this->rows);
$this->rows = null;
$this->cols = null;
$this->index = -1;
$this->origIndex = -1;
$this->key = null;
$this->raw = null;
$this->row = null;
# ne pas toucher à data, l'utilisateur peut vouloir continuer à consulter
# les valeurs
}
return $valid;
}
function current(): ?array {
return $this->row;
}
function key() {
return $this->key;
}
function next() {
iter::next($this->rows);
$this->index++;
$this->origIndex++;
}
#############################################################################
function each(callable $func): void {
$func = func::with($func);
$this->rewind();
while ($this->valid()) {
$func->invoke([$this]);
$this->next();
}
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IComponent;
abstract class AbstractComponent implements IComponent {
use TComponentPhase;
const COMPONENT_PHASE_CAN_PREPARE = false;
const COMPONENT_PHASE_CAN_CONFIGURE = false;
const COMPONENT_PHASE_CAN_SETUP = false;
const COMPONENT_PHASE_CAN_TEARDOWN = false;
function RENDERER_CLASS(): ?string {
return static::RENDERER_CLASS;
} const RENDERER_CLASS = null;
}

View File

@ -0,0 +1,15 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IPage;
abstract class AbstractPage extends AbstractComponent implements IPage {
function USE_SESSION(): bool {
return static::USE_SESSION;
} const USE_SESSION = false;
/** faut-il fermer automatiquement la session avant l'affichage? */
protected function AUTOCLOSE_SESSION(): bool {
return static::AUTOCLOSE_SESSION;
} const AUTOCLOSE_SESSION = true;
}

View File

@ -0,0 +1,5 @@
<?php
namespace nulib\web\base;
class BasicPage extends AbstractPage {
}

View File

@ -0,0 +1,246 @@
<?php
namespace nulib\web\base;
use nulib\exceptions;
use nulib\php\content\c;
use nulib\str;
use nulib\web\ly;
use nulib\web\model\IPage;
use nulib\web\model\IRenderer;
use nulib\web\page;
class Html5PageRenderer implements IRenderer {
function render($page): void {
if ($page instanceof IPage) {
page::set_html_output();
$this->page = $page;
$this->printStartHtml();
$this->printStartHead();
$this->printCssLinks();
$this->printCss();
$this->printJsLinks();
$this->printJs();
$this->printScript();
$this->printHeadTitle();
$this->printEndHead();
$this->printStartBody();
$this->printEndBody();
$this->printEndHtml();
} else {
throw exceptions::invalid_type($page, "page", IPage::class);
}
}
protected IPage $page;
function printStartHtml(): void {
?>
<!DOCTYPE html>
<html lang="fr" xmlns="http://www.w3.org/1999/xhtml">
<?php
}
function printStartHead(): void {
$prefix = $this->page->getSelfRelativePrefix();
?>
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="<?=$prefix?>nur-base/base.css" rel="stylesheet"/>
<?php
}
function printCssLinks(): void {
$prefix = $this->page->getSelfRelativePrefix();
foreach ($this->cssUrls as $url) {
$url = prefix::add($url, $prefix);
?>
<link href="<?=$url?>" rel="stylesheet"/>
<?php
}
}
function printCss(): void {
foreach ($this->getPlugins() as $plugin) {
$plugin->printCss();
}
}
function printJsLinks(): void {
$prefix = $this->page->getSelfRelativePrefix();
foreach ($this->jsUrls as $url) {
$url = prefix::add($url, $prefix);
?>
<script src="<?=$url?>" type="text/javascript"></script>
<?php
}
}
function printJs(): void {
foreach ($this->getPlugins() as $plugin) {
$plugin->printJs();
}
}
protected function beforeCapture(): void {
ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE);
}
private static function strip_lines(array &$lines): void {
while (count($lines) > 0 && !$lines[0]) {
# enlever les lignes vides au début
array_shift($lines);
}
while (($count = count($lines)) > 0 && !$lines[$count - 1]) {
# enlever les lignes vides à la fin
array_pop($lines);
}
}
const RE_START_SCRIPT = '/^\s*<script\s+type="(?:text\/)?javascript">\s*(.*)/';
const RE_END_SCRIPT = '/(.*?)\s*<\/script>\s*$/';
private static function unwrap_script(array &$lines): void {
$count = count($lines);
if ($count >= 2) {
$first = $lines[0];
$last = $lines[$count - 1];
if (preg_match(self::RE_START_SCRIPT, $first)
&& preg_match(self::RE_END_SCRIPT, $last)) {
$last = preg_replace(self::RE_END_SCRIPT, '$1', $last);
if ($last) $lines[$count - 1] = $last;
else $lines = array_slice($lines, 0, $count - 1);
$first = preg_replace(self::RE_START_SCRIPT, '$1', $first);
if ($first) $lines[0] = $first;
else $lines = array_slice($lines, 1);
}
}
}
protected $scripts = [];
protected function captureScript(): void {
$lines = trim(ob_get_clean());
if ($lines) {
$lines = str::split_nl($lines);
self::strip_lines($lines);
self::unwrap_script($lines);
$this->scripts[] = implode("\n", $lines);
}
}
const RE_START_JQUERY = '/^\s*jQuery(?:.noConflict\(\))?\(function\(\$\) {\s*(.*)/';
const RE_END_JQUERY = '/(.*?)\s*}\);\s*$/';
private static function unwrap_jquery(array &$lines): void {
$count = count($lines);
if ($count >= 2) {
$first = $lines[0];
$last = $lines[$count - 1];
if (preg_match(self::RE_START_JQUERY, $first)
&& preg_match(self::RE_END_JQUERY, $last)) {
$last = preg_replace(self::RE_END_JQUERY, '$1', $last);
if ($last) $lines[$count - 1] = $last;
else $lines = array_slice($lines, 0, $count - 1);
$first = preg_replace(self::RE_START_JQUERY, '$1', $first);
if ($first) $lines[0] = $first;
else $lines = array_slice($lines, 1);
}
}
}
protected $jqueries = [];
protected function captureJquery(): void {
$lines = trim(ob_get_clean());
if ($lines) {
$lines = str::split_nl($lines);
self::strip_lines($lines);
self::unwrap_script($lines);
self::unwrap_jquery($lines);
$this->jqueries[] = implode("\n", $lines);
}
}
protected function resolvePluginsScripts(): void {
foreach ($this->getPlugins() as $plugin) {
if ($plugin->haveScript()) {
$this->beforeCapture();
$plugin->printScript();
$this->captureScript();
}
if ($plugin->haveJquery()) {
$this->beforeCapture();
$plugin->printJquery();
$this->captureJquery();
}
}
}
protected function printMergedScripts(): void {
$scripts = $this->scripts;
$jqueries = $this->jqueries;
if ($scripts || $jqueries) {
?>
<script type="text/javascript">
<?php
foreach ($scripts as $script) {
echo "$script\n";
}
}
if ($jqueries) {
?>
jQuery.noConflict()(function($) {
<?php
foreach ($jqueries as $jquery) {
echo "$jquery\n";
}
?>
});
<?php
}
if ($scripts || $jqueries) {
?>
</script>
<?php
}
}
function printScript(): void {
$this->resolvePluginsScripts();
$this->printMergedScripts();
}
function printHeadTitle(): void {
$title = c::to_string($this->page->TITLE());
echo "<title>$title</title>\n";
}
function printEndHead(): void {
?>
</head>
<?php
}
function printStartBody(): void {
?>
<body>
<?php
}
function printContent(): void {
c::write($this->page);
ly::end();
}
function printEndBody(): void {
?>
</body>
<?php
}
function printEndHtml(): void {
?>
</html>
<?php
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace nulib\web\base;
use nulib\php\content\c;
use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
use nulib\web\model\IRenderer;
class HtmlRenderer implements IRenderer {
function render($data): void {
header("Content-Type: text/html; charset=utf-8");
if ($data instanceof IPrintable || $data instanceof IContent) {
c::write([$data]);
} else {
echo $data;
}
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IRenderer;
use nur\json;
class JsonRenderer implements IRenderer {
function render($data): void {
header("Content-Type: application/json");
if (is_iterable($data)) {
echo "[";
$sep = "";
foreach ($data as $datum) {
$line = json::encode($datum);
echo "$sep$line\n";
$sep = ",";
}
echo "]";
} else {
echo json::encode($data);
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace nulib\web\base;
use nulib\web\page;
trait TComponentPhase {
protected int $currentComponentPhase;
function beforeComponentPhase(int $phase): bool {
return true;
}
function doComponentPhase(int $phase) {
$this->currentComponentPhase = $phase;
switch ($phase) {
case page::PHASE_PREPARE:
if (static::COMPONENT_PHASE_CAN_PREPARE) $this->doComponentPhasePrepare();
break;
case page::PHASE_CONFIGURE:
if (static::COMPONENT_PHASE_CAN_CONFIGURE) $this->doComponentPhaseConfigure();
break;
case page::PHASE_SETUP:
if (static::COMPONENT_PHASE_CAN_SETUP) return $this->doComponentPhaseSetup();
case page::PHASE_TEARDOWN:
if (static::COMPONENT_PHASE_CAN_TEARDOWN) $this->doComponentPhaseTeardown();
break;
}
return null;
}
function afterComponentPhase(int $phase): void {
}
function didComponentPhase(int $phase): bool {
return $phase >= $this->currentComponentPhase;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IRenderer;
class TextRenderer implements IRenderer {
function render($data): void {
header("Content-Type: text/plain; charset=utf-8");
echo $data;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace nulib\web\content;
class BlockTag extends Tag {
const START_PREFIX = null;
const START_SUFFIX = null;
const END_PREFIX = null;
const END_SUFFIX = "\n";
protected ?string $startPrefix;
protected ?string $startSuffix;
protected ?string $endPrefix;
protected ?string $endSuffix;
function reset(string $tag, $content=null, ?array $params=null): self {
parent::reset($tag, $content, $params);
$this->startPrefix = $params["start_prefix"] ?? self::START_PREFIX;
$this->startSuffix = $params["start_suffix"] ?? self::START_SUFFIX;
$this->endPrefix = $params["end_prefix"] ?? self::END_PREFIX;
$this->endSuffix = $params["end_suffix"] ?? self::END_SUFFIX;
return $this;
}
function getStart(): array {
return [$this->startPrefix, ...parent::getStart(), $this->startSuffix];
}
function getEnd(): array {
return [$this->endPrefix, ...parent::getEnd(), $this->endSuffix];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace nulib\web\content;
use nulib\A;
use nulib\php\content\c;
class EmptyTag extends SimpleTag {
function getContent($objectOrClass=null): iterable {
$attrs = [];
$children = [];
$this->resolveContents($attrs, $children, $this->contents);
$parts = ["<{$this->tag}"];
$this->addAttrs($parts, $attrs);
if ($children) {
$parts[] = ">";
A::merge($parts, $children);
$parts[] = "</{$this->tag}>";
} else {
$parts[] = "/>";
}
return [c::to_string($parts)];
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace nulib\web\content;
use Closure;
use nulib\A;
use nulib\cl;
use nulib\php\content\c;
use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
use nulib\php\func;
use nur\str;
class SimpleTag implements IContent {
function __construct(string $tag, $content=null) {
$this->reset($tag, $content);
}
protected string $tag;
protected iterable $contents;
function reset(string $tag, $content=null, ?array $params=null): self {
$this->tag = $tag;
$this->contents = c::q($content);
return $this;
}
function add($content): self {
if (!is_array($this->contents)) {
# si c'est un itérable, le mettre en tant qu'élément dans le tableau
$contents = $this->contents;
$this->contents = [static function() use ($contents) {
return $contents;
}];
}
A::merge($this->contents, c::q($content));
return $this;
}
protected function resolveContents(array &$attrs, array &$children, iterable $contents): void {
$index = 0;
foreach ($contents as $key => $content) {
if ($content instanceof Closure) {
# les closure sont appelés dès la résolution
$content = func::with($content)->invoke([$this]);
if ($key === $index) {
$index++;
# le résultat est inséré tels quels dans le flux
if (is_iterable($content)) {
$children[] = $content;
} else {
A::merge($children, cl::with($content));
}
} else {
# le résultat est la valeur de l'attribut
A::merge($attrs[$key], [$content]);
}
} elseif (is_array($content)) {
if ($key === $index) {
$index++;
$this->resolveContents($attrs, $children, $content);
} else {
foreach ($content as &$value) {
if ($value instanceof Closure) {
$value = func::with($value)->invoke([$this]);
}
}; unset($value);
A::merge($attrs[$key], $content);
}
} elseif ($key === $index) {
$index++;
A::merge($children, cl::with($content));
} else {
A::merge($attrs[$key], [$content]);
}
}
}
protected function addAttrs(array &$parts, array $attrs): void {
foreach ($attrs as $key => $value) {
if ($value === null || $value === false) continue;
if ($value === true || $value === [true]) {
$value = $key;
} elseif (is_array($value)) {
$value = str::join3($value);
if (!$value) continue;
}
$parts[] = " ";
$parts[] = $key;
$parts[] = "=\"";
$parts[] = htmlspecialchars($value);
$parts[] = "\"";
}
}
function getContent($objectOrClass=null): iterable {
$attrs = [];
$children = [];
$this->resolveContents($attrs, $children, $this->contents);
$parts = ["<{$this->tag}"];
$this->addAttrs($parts, $attrs);
$parts[] = ">";
return [c::to_string([
implode("", $parts),
...$children,
"</{$this->tag}>",
])];
}
}

View File

@ -2,35 +2,97 @@
namespace nulib\web\content;
use nulib\A;
use nulib\php\content\c;
use nulib\php\content\IContent;
use nulib\php\content\IPrintable;
class Tag implements IContent {
function __construct(string $tag, $content=null) {
$this->tag = $tag;
$content = c::q($content);
$this->content = $content;
class Tag extends SimpleTag implements IPrintable {
const REQUIRE_CONTENT = false;
function __construct(string $tag, ?array $params=null, $content=null) {
$this->reset($tag, $content, $params);
}
protected string $tag;
protected iterable $content;
protected bool $requireContent;
function add($content): self {
if (!is_array($this->content)) {
# si c'est un itérable, l'inclure avec un merge statique, afin de pouvoir
# rajouter des éléments
$this->content = [[[], $this->content]];
}
A::merge($this->content, c::q($content));
protected ?array $attrs = null;
protected ?array $children = null;
function reset(string $tag, $content=null, ?array $params=null): self {
parent::reset($tag, $content, $params);
$this->requireContent = $params["require_content"] ?? self::REQUIRE_CONTENT;
$this->attrs = null;
$this->children = null;
return $this;
}
function getContent($object_or_class=null): iterable {
function add($content): self {
parent::add($content);
$this->attrs = null;
$this->children = null;
return $this;
}
function resolve($objectOrClass=null): self {
if ($this->attrs === null) {
$attrs = [];
$children = [];
$this->resolveContents($attrs, $children, $this->contents);
$this->attrs = $attrs;
$this->children = c::resolve($children, $objectOrClass);
}
return $this;
}
function getStart(): array {
$parts = ["<{$this->tag}"];
$this->addAttrs($parts, $this->attrs);
$parts[] = ">";
return [implode("", $parts)];
}
function getChildren(): array {
return $this->children;
}
function getEnd(): array {
return ["</{$this->tag}>"];
}
function getContent($objectOrClass=null): iterable {
$this->resolve($objectOrClass);
if ($this->requireContent && !$this->children) return [];
return [
"<$this->tag>",
...c::resolve($this->content, $object_or_class),
"</$this->tag>",
...$this->getStart(),
...$this->getChildren(),
...$this->getEnd(),
];
}
/** afficher le tag ouvrant. */
function printStart(): void {
$this->resolve();
c::write($this->getStart());
}
/** afficher le contenu enfant */
function printChildren(): void {
$this->resolve();
c::write($this->getChildren());
}
/** afficher le tag fermant */
function printEnd(): void {
$this->resolve();
c::write($this->getEnd());
}
/** afficher le tag et le contenu enfant */
function print(): void {
$this->resolve();
if ($this->requireContent && !$this->children) return;
c::write($this->getStart());
c::write($this->getChildren());
c::write($this->getEnd());
}
}

View File

@ -5,6 +5,65 @@ namespace nulib\web\content;
* Class v: classe outil pour gérer du contenu pour le web
*/
class v {
static function h1($content): iterable { return (new Tag("h1", $content))->getContent(); }
const h1 = [Tag::class, null, "h1"];
private const require_content = ["require_content" => true];
private const start_nl = ["start_suffix" => "\n"];
private static function h(string $tag, $content): BlockTag {
return new BlockTag($tag, self::require_content, $content);
}
static function h1($content): BlockTag { return self::h("h1", $content); }
const H1 = [BlockTag::class, false, "h1", self::require_content];
static function h2($content): BlockTag { return self::h("h2", $content); }
const H2 = [BlockTag::class, false, "h2", self::require_content];
static function h3($content): BlockTag { return self::h("h3", $content); }
const H3 = [BlockTag::class, false, "h3", self::require_content];
static function h4($content): BlockTag { return self::h("h4", $content); }
const H4 = [BlockTag::class, false, "h4", self::require_content];
static function h5($content): BlockTag { return self::h("h5", $content); }
const H5 = [BlockTag::class, false, "h5", self::require_content];
static function h6($content): BlockTag { return self::h("h6", $content); }
const H6 = [BlockTag::class, false, "h6", self::require_content];
static function hr($content=null): EmptyTag { return (new EmptyTag("hr", $content)); }
const HR = [EmptyTag::class, false, "hr"];
static function br($content=null): EmptyTag { return (new EmptyTag("br", $content)); }
const BR = [EmptyTag::class, false, "br"];
static function div($content): Tag { return (new Tag("div", null, $content)); }
const DIV = [Tag::class, false, "div", null];
static function p($content): Tag { return (new Tag("p", self::require_content, $content)); }
const P = [Tag::class, false, "p", self::require_content];
static function pre($content): Tag { return (new Tag("pre", self::require_content, $content)); }
const PRE = [Tag::class, false, "pre", self::require_content];
static function span($content): SimpleTag { return (new SimpleTag("span", $content)); }
const SPAN = [SimpleTag::class, false, "span"];
static function b($content): SimpleTag { return (new SimpleTag("b", $content)); }
const B = [SimpleTag::class, false, "b"];
static function i($content): SimpleTag { return (new SimpleTag("i", $content)); }
const I = [SimpleTag::class, false, "i"];
static function em($content): SimpleTag { return (new SimpleTag("em", $content)); }
const EM = [SimpleTag::class, false, "em"];
static function strong($content): SimpleTag { return (new SimpleTag("strong", $content)); }
const STRONG = [SimpleTag::class, false, "strong"];
static function ul($content): BlockTag { return (new BlockTag("ul", self::start_nl, $content)); }
const UL = [BlockTag::class, false, "ul", self::start_nl];
static function ol($content): BlockTag { return (new BlockTag("ol", self::start_nl, $content)); }
const OL = [BlockTag::class, false, "ol", self::start_nl];
static function li($content): BlockTag { return (new BlockTag("li", null, $content)); }
const LI = [BlockTag::class, false, "li", null];
static function table($content): BlockTag { return (new BlockTag("table", self::start_nl, $content)); }
const TABLE = [BlockTag::class, false, "table", self::start_nl];
static function thead($content): BlockTag { return (new BlockTag("thead", self::start_nl, $content)); }
const THEAD = [BlockTag::class, false, "thead", self::start_nl];
static function tbody($content): BlockTag { return (new BlockTag("tbody", self::start_nl, $content)); }
const TBODY = [BlockTag::class, false, "tbody", self::start_nl];
static function tr($content): BlockTag { return (new BlockTag("tr", null, $content)); }
const TR = [BlockTag::class, false, "tr", null];
static function th($content): Tag { return (new Tag("th", null, $content)); }
const TH = [Tag::class, false, "th", null];
static function td($content): Tag { return (new Tag("td", null, $content)); }
const TD = [Tag::class, false, "td", null];
}

78
src/web/layout/README.md Normal file
View File

@ -0,0 +1,78 @@
# nulib\web\layout
faire le layout d'avance, e.g
~~~php
ly::prepare([
["row", "class" => "row-gap",
["col", 2, "content" => "a"],
["col", 10, "content" => "b"],
],
["row",
["col", 6, "content" => "c"],
["col", 6, "content" => "d"],
],
]);
~~~
dans cet exemple, il y a 4 sections de contenu appelées "a", "b", "c" et "d"
désactiver le contenu dans un colonne ou un row avec `"content" => false`
faut-il prévoir d'autres types que "row" et "col", par exemple "panel"?
une fois que le layout est fait, on sélectionne les sections avant de les remplir
~~~php
ly::start("a");
//... contenu de la section "a"
ly::start("b");
//... contenu de la section "b"
ly::end();
~~~
tant que les sections sont mentionnées dans l'ordre, l'affichage se fait au fur
et à mesure
*éventuellement*, supporter un mode où les sections sont remplies dans un ordre
quelconque. dans ce cas, le contenu est enregistré dans un fichier temporaire
mémoire avec `ob_start()` et `ob_end()` puis il est affiché à la fin lors de
ly::end()
exemple:
~~~php
ly::prepare([["row",
["col", 3, "content" => "menu"],
["col", 9, "content" => "details"],
]]);
foreach ($items as $item) {
ly::start("menu");
write(link);
ly::start("details");
write(details);
}
~~~
## alternatives
les ids de contenu sont des clés
~~~php
ly::prepare([["row",
"menu" => ["col", 3],
"details" => ["col", 9],
]]);
~~~
conflit possible avec `"class" => xxx` et autres attributs?
`ly::start($id, $func)` permet de basculer temporairement dans une section
~~~php
ly::start("details");
foreach ($items as $item) {
ly::start("menu", function() {
write(link);
});
write(details);
}
~~~
le comportement d'enregistrer le contenu devrait être demandé explicitement
* true
* false: exception si une section n'est pas remplie avant de passer à la suivante
* auto: activé si les sections sont accédées dans un ordre différent du naturel
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

7
src/web/ly.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace nulib\web;
class ly {
static function end(): void {
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\web\model;
use nulib\php\content\IContent;
interface IComponent extends IContent {
/** retourner la classe de renderer à utiliser pour afficher ce composant */
function RENDERER_CLASS(): ?string;
function beforeComponentPhase(int $phase): bool;
function doComponentPhase(int $phase);
function afterComponentPhase(int $phase): void;
function didComponentPhase(int $phase): bool;
}

31
src/web/model/IPage.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace nulib\web\model;
use nulib\php\content\IPrintable;
interface IPage extends IComponent, IPrintable {
/** indiquer si cette page a besoin d'avoir une session valide */
function USE_SESSION(): bool;
/**
* retourner le chemin vers le *script* courant par rapport à la racine de
* l'application, e.g 'index.php' s'il est différent de la valeur par défaut
* basename($_SERVER["SCRIPT_NAME"]), ou null pour utiliser la valeur par
* défaut
*
* celà permet de calculer les chemins relatifs aux resources, même si
* l'application n'est pas servie à la racine ou si on utilise path_info
*/
function SELF(): ?string;
/**
* retourner le titre de la page, ou null pour utiliser la valeur par défaut
*/
function TITLE(): ?string;
/**
* retourner le préfixe à rajouter aux chemins relatifs exprimés depuis la
* racine pour les atteindre depuis la page courante
*/
function getSelfRelativePrefix(): string;
}

View File

@ -0,0 +1,6 @@
<?php
namespace nulib\web\model;
interface IRenderer {
function render($data): void;
}

65
src/web/page.php Normal file
View File

@ -0,0 +1,65 @@
<?php
namespace nulib\web;
use nulib\app\web\Application;
use nulib\web\model\IComponent;
use nulib\web\model\IRenderer;
class page {
protected static ?Application $app = null;
static function set_app(Application $app) {
self::$app = $app;
}
static function get_app(): ?Application {
return self::$app;
}
protected static ?IComponent $component = null;
static function set_component(IComponent $component) {
self::$component = $component;
}
static function get_component(): ?IComponent {
return self::$component;
}
const PHASE_NONE = 0;
const PHASE_PREPARE = 1;
const PHASE_CONFIGURE = 2;
const PHASE_SETUP = 3;
const PHASE_PRINT = 4;
const PHASE_TEARDOWN = 5;
protected static int $component_phase = self::PHASE_NONE;
static function set_component_phase(int $component_phase): void {
self::$component_phase = $component_phase;
}
static function get_component_phase(): int {
return self::$component_phase;
}
protected static ?IRenderer $renderer = null;
static function set_renderer(IRenderer $renderer): void {
self::$renderer = $renderer;
}
static function get_renderer(): ?IRenderer {
return self::$renderer;
}
protected static bool $html_output = false;
static function set_html_output(): void {
self::$html_output = true;
}
static function is_html_output(): bool {
return self::$html_output;
}
}

44
src/web/vo.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace nulib\web;
use nulib\php\content\c;
use nulib\web\content\BlockTag;
use nulib\web\content\EmptyTag;
use nulib\web\content\SimpleTag;
use nulib\web\content\Tag;
use nulib\web\content\v;
/**
* Class vo: classe outil pour afficher les tags générés par {@link v}
*
* @method BlockTag h1($content)
* @method BlockTag h2($content)
* @method BlockTag h3($content)
* @method BlockTag h4($content)
* @method BlockTag h5($content)
* @method BlockTag h6($content)
* @method EmptyTag hr($content=null)
* @method EmptyTag br($content=null)
* @method Tag div($content)
* @method Tag p($content)
* @method Tag pre($content)
* @method SimpleTag span($content)
* @method SimpleTag b($content)
* @method SimpleTag i($content)
* @method SimpleTag em($content)
* @method SimpleTag strong($content)
* @method BlockTag ul($content)
* @method BlockTag ol($content)
* @method BlockTag li($content)
* @method BlockTag table($content)
* @method BlockTag thead($content)
* @method BlockTag tbody($content)
* @method BlockTag tr($content)
* @method Tag th($content)
* @method Tag td($content)
*/
class vo {
static function __callStatic($name, $args) {
c::write(call_user_func_array([v::class, $name], $args));
}
}

12
tbin/test-vo.php Normal file
View File

@ -0,0 +1,12 @@
<?php
require __DIR__ . "/../vendor/autoload.php";
use nulib\web\content\v;
use nulib\web\vo;
vo::h1("titre");
vo::p("paragraph");
vo::p([
"Il y a ", v::b(4), " lapins.",
"Allons à la plage",
]);

View File

@ -1,152 +0,0 @@
<?php
namespace nulib\php\coll;
use Exception;
use nulib\cl;
use nulib\tests\TestCase;
use TypeError;
class CursorTest extends TestCase {
function test_map_row() {
$cursor = new class extends Cursor {
function mapRow(array $row, ?array $map): array {
return cl::map($row, $map);
}
};
$row = ["a" => 1, "b" => 2, "c" => 3, "x" => 99];
$map = ["a", "b" => "x", "c" => function() { return "y"; }, "d" => null];
self::assertSame([
"a" => $row["a"],
"b" => $row["x"],
"c" => "y",
"d" => null
], $cursor->mapRow($row, $map));
}
function test_filter_row() {
$cursor = new class extends Cursor {
function filterRow(array $row, $filter): bool {
return cl::filter($row, $filter);
}
};
$row = ["a" => 1, "b" => 2, "c" => 3, "x" => 99];
self::assertTrue($cursor->filterRow($row, "a"));
self::assertTrue($cursor->filterRow($row, ["a"]));
self::assertTrue($cursor->filterRow($row, ["a" => true]));
self::assertFalse($cursor->filterRow($row, ["a" => false]));
self::assertTrue($cursor->filterRow($row, ["a" => 1]));
self::assertFalse($cursor->filterRow($row, ["a" => 2]));
self::assertFalse($cursor->filterRow($row, "z"));
self::assertFalse($cursor->filterRow($row, ["z"]));
self::assertFalse($cursor->filterRow($row, ["z" => true]));
self::assertTrue($cursor->filterRow($row, ["z" => false]));
self::assertFalse($cursor->filterRow($row, ["z" => 1]));
}
const SCALARS = [0, 1, 2, 3, 4];
function generator() {
yield from self::SCALARS;
}
function testVanilla() {
$c = new Cursor(self::SCALARS);
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
$c = new Cursor($this->generator());
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
self::assertException(Exception::class, function() use ($c) {
// pas possible de rewind un générateur
return cl::all($c);
});
$c = new Cursor(null, [
"rows" => function() {
return self::SCALARS;
},
]);
self::assertError(TypeError::class, function() use ($c) {
// rows doit être un iterable, pas une fonction
return cl::all($c);
});
$c = new Cursor(null, [
"rows" => $this->generator(),
]);
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
self::assertException(Exception::class, function() use ($c) {
// pas possible de rewind un générateur
return cl::all($c);
});
$c = new Cursor(null, [
"rows_func" => function() {
return self::SCALARS;
},
]);
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
$c = new Cursor(null, [
"rows_func" => $this->generator(),
]);
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
self::assertException(Exception::class, function() use ($c) {
// pas possible de rewind un générateur
return cl::all($c);
});
$c = new Cursor(null, [
"rows_func" => function() {
yield from self::SCALARS;
},
]);
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
self::assertSame([[0], [1], [2], [3], [4]], cl::all($c));
}
function testMap() {
$c = new Cursor(self::SCALARS, [
"map_func" => function(Cursor $c) {
return [$c->raw + 1];
},
]);
self::assertSame([[1], [2], [3], [4], [5]], cl::all($c));
}
function testFilter() {
$c = new Cursor(self::SCALARS, [
"filter_func" => function(Cursor $c) {
return $c->raw % 2 == 0;
},
]);
self::assertSame([[1], [3]], cl::all($c));
}
function testEach() {
$c = new Cursor(self::SCALARS, [
"filter_func" => function(Cursor $c) {
return $c->raw % 2 == 0;
},
"map_func" => function(Cursor $c) {
return [$c->raw + 1];
},
]);
$xs = [];
$xitems = [];
$oxs = [];
$kitems = [];
$c->each(function(Cursor $c) use (&$xs, &$xitems, &$oxs, &$kitems) {
$xs[] = $c->index;
$oxs[] = $c->origIndex;
$xitems[$c->index] = $c->row[0];
$kitems[$c->key] = $c->row[0];
});
self::assertSame([0, 1], $xs);
self::assertSame([2, 4], $xitems);
self::assertSame([1, 3], $oxs);
self::assertSame([1 => 2, 3 => 4], $kitems);
}
}

View File

@ -1,9 +1,10 @@
<?php
namespace nulib\php\content;
use nulib\php\content\impl\html;
use nulib\php\content\impl\AContent;
use nulib\php\content\impl\APrintable;
use nulib\tests\TestCase;
use nulib\web\content\v;
use PHPUnit\Framework\TestCase;
class cTest extends TestCase {
function testTo_string() {
@ -20,21 +21,24 @@ class cTest extends TestCase {
self::assertSame("hello. world", c::to_string(["hello.", "world"]));
self::assertSame("hello.<world>", c::to_string(["hello.", "<world>"]));
self::assertSame(
"<h1>title&lt;q/&gt;</h1><p>hello<nq/><span>brave&lt;q/&gt;</span><span>world<nq/></span></p>",
c::to_string([
[html::H1, "title<q/>"],
[html::P, [
"hello<nq/>",
[html::SPAN, "brave<q/>"],
[html::SPAN, ["world<nq/>"]],
]],
]));
}
self::assertSame(<<<EOT
<h1>title&lt;q/&gt;</h1>
<p>hello<nq/><span>brave&lt;q/&gt;</span><span>world<nq/></span></p>
EOT, c::to_string([
[v::H1, "title<q/>"],
[v::P, [
"hello<nq/>",
[v::SPAN, "brave<q/>"],
[v::SPAN, ["world<nq/>"]],
]],
]));
function testXxx() {
$content = [[v::h1, "hello"]];
self::assertSame("<h1>hello</h1>", c::to_string($content));
self::assertSame(<<<EOT
<span>content</span><p>printable</p>
EOT, c::to_string([
new AContent(),
new APrintable(),
]));
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace nulib\php\content\impl;
use nulib\php\content\c;
use nulib\php\content\IContent;
class ATag implements IContent {
function __construct(string $tag, $content=null) {
$this->tag = $tag;
$this->content = $content;
}
protected $tag;
protected $content;
function getContent(): iterable {
return [
"<$this->tag>",
...c::q($this->content),
"</$this->tag>",
];
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace nulib\php\content\impl;
class html {
const H1 = [self::class, "h1"];
const DIV = [self::class, "div"];
const P = [self::class, "p"];
const SPAN = [self::class, "span"];
static function h1($content) { return new ATag("h1", $content); }
static function div($content) { return new ATag("div", $content); }
static function p($content) { return new ATag("p", $content); }
static function span($content) { return new ATag("span", $content); }
}

View File

@ -0,0 +1,82 @@
<?php
namespace nulib\web\content;
use nulib\php\content\c;
use nulib\tests\TestCase;
class TagTest extends TestCase {
function testTag() {
$tag = new Tag("tag", null, [
"before",
"class" => "first",
["class" => "second"],
function () {
return 42;
},
"attr" => [
"static",
"true" => true,
"false" => false,
],
"after",
]);
self::assertSame([
"<tag",
" ", "class", "=\"", "first second", "\"",
" ", "attr", "=\"", "static true", "\"",
">",
"before",
42,
"after",
"</tag>",
], $tag->getContent());
self::assertSame('<tag class="first second" attr="static true">before 42 after</tag>', c::to_string($tag));
}
function testMerge() {
$tag = new Tag("tag", null, [
"class" => "first",
["class" => "second"],
["class" => function () {
return "third";
}],
"cond" => [
"base",
"ok" => true,
"ko" => false,
"dynok" => function () {
return true;
},
"dynko" => function () {
return false;
},
],
["plouf" => "base"],
["plouf" => [
"ok" => true,
"ko" => false,
]],
["plouf" => [
"dynok" => function () {
return true;
},
"dynko" => function () {
return false;
},
]],
]);
self::assertSame([
"<tag",
" ", "class", "=\"", "first second third", "\"",
" ", "cond", "=\"", "base ok dynok", "\"",
" ", "plouf", "=\"", "base ok dynok", "\"",
">",
"</tag>",
], $tag->getContent());
self::assertSame('<tag class="first second third" cond="base ok dynok" plouf="base ok dynok"></tag>', c::to_string($tag));
}
}

123
tests/web/content/vTest.php Normal file
View File

@ -0,0 +1,123 @@
<?php
namespace nulib\web\content;
use nulib\cl;
use nulib\php\content\c;
use nulib\tests\TestCase;
class vTest extends TestCase {
function testStatic() {
self::assertSame(<<<EOT
<h1>title</h1>
<p>text</p>
EOT, c::to_string([
[v::H1, "title"],
[v::P, "text"],
]));
self::assertSame(<<<EOT
<h1>title</h1>
<p>the<b>bold</b>text<br/>linefeed</p><hr/>
EOT, c::to_string([
[v::H1, "title"],
[v::P, [
"the",
[v::B, "bold"],
"text",
[v::BR],
"linefeed",
]],
[v::HR],
]));
self::assertSame(<<<EOT
<div class="div" checked="checked">before<span>spanned</span><span class="black">blacked</span>after</div>
EOT, c::to_string([
[v::DIV, [
"before",
"class" => "div",
[v::SPAN, "spanned"],
[v::SPAN, ["class" => "black", "blacked"]],
"disabled" => false,
"checked" => true,
"after",
]],
]));
}
function testDynamic() {
self::assertSame(<<<EOT
<h1>title</h1>
<p>text</p>
EOT, c::to_string([
v::h1("title"),
v::p("text"),
]));
self::assertSame(<<<EOT
<h1>title</h1>
<p>the<b>bold</b>text<br/>linefeed</p><hr/>
EOT, c::to_string([
v::h1("title"),
v::p([
"the",
v::b("bold"),
"text",
v::br(),
"linefeed",
]),
v::hr(),
]));
self::assertSame(<<<EOT
<div class="div" checked="checked">before<span>spanned</span><span class="black">blacked</span>after</div>
EOT, c::to_string([
v::div([
"before",
"class" => "div",
v::span("spanned"),
v::span(["class" => "black", "blacked"]),
"disabled" => false,
"checked" => true,
"after",
]),
]));
}
function testDynamic2() {
$rows = [
["a" => 1, "b" => 2, "c" => 3],
["a" => "un", "b" => "deux", "c" => "trois"],
["a" => "one", "b" => "two", "c" => "three"],
];
self::assertSame(<<<EOT
<table>
<thead>
<tr><th>a</th><th>b</th><th>c</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td><td>3</td></tr>
<tr><td>un</td><td>deux</td><td>trois</td></tr>
<tr><td>one</td><td>two</td><td>three</td></tr>
</tbody>
</table>
EOT, c::to_string(v::table([
v::thead(v::tr(function() use ($rows) {
$headers = array_keys(cl::first($rows));
foreach ($headers as $header) {
yield v::th($header);
}
})),
v::tbody(function() use ($rows) {
foreach ($rows as $row) {
yield v::tr(function () use ($row) {
foreach ($row as $col) {
yield v::td($col);
}
});
}
}),
])));
}
}