Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 079c154ef2 | |||
| 7416b28032 | |||
| b9df5da464 | |||
| b99cb3a112 | |||
| e59b136b64 | |||
| 9b83f276fe | |||
| 944daa8ea4 | |||
| 0f6a76e701 | |||
| bff1953f9e | |||
| b09d01c37a | |||
| b274372c41 | |||
| d17b826b5a | |||
| 1c45bcb4af | |||
| 6d60131122 | |||
| b11cc50143 |
@ -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
|
||||
|
||||
@ -64,6 +64,8 @@ git checkout dev74
|
||||
git cherry-pick "$commit"
|
||||
pp -a
|
||||
|
||||
_merge82
|
||||
|
||||
## console 82
|
||||
|
||||
pu
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
use cli\Csv2xlsxApp;
|
||||
|
||||
Csv2xlsxApp::run();
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
73
composer.lock
generated
@ -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": {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
236
src/app/web/Application.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
285
src/app/web/RouteManager.php
Normal file
285
src/app/web/RouteManager.php
Normal 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é");
|
||||
}
|
||||
}
|
||||
168
src/php/iter.php
168
src/php/iter.php
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/web/base/AbstractComponent.php
Normal file
17
src/web/base/AbstractComponent.php
Normal 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;
|
||||
}
|
||||
15
src/web/base/AbstractPage.php
Normal file
15
src/web/base/AbstractPage.php
Normal 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;
|
||||
}
|
||||
5
src/web/base/BasicPage.php
Normal file
5
src/web/base/BasicPage.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
namespace nulib\web\base;
|
||||
|
||||
class BasicPage extends AbstractPage {
|
||||
}
|
||||
246
src/web/base/Html5PageRenderer.php
Normal file
246
src/web/base/Html5PageRenderer.php
Normal 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
|
||||
}
|
||||
}
|
||||
18
src/web/base/HtmlRenderer.php
Normal file
18
src/web/base/HtmlRenderer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/web/base/JsonRenderer.php
Normal file
23
src/web/base/JsonRenderer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/web/base/TComponentPhase.php
Normal file
37
src/web/base/TComponentPhase.php
Normal 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;
|
||||
}
|
||||
}
|
||||
11
src/web/base/TextRenderer.php
Normal file
11
src/web/base/TextRenderer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
33
src/web/content/BlockTag.php
Normal file
33
src/web/content/BlockTag.php
Normal 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];
|
||||
}
|
||||
}
|
||||
23
src/web/content/EmptyTag.php
Normal file
23
src/web/content/EmptyTag.php
Normal 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)];
|
||||
}
|
||||
}
|
||||
110
src/web/content/SimpleTag.php
Normal file
110
src/web/content/SimpleTag.php
Normal 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}>",
|
||||
])];
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
78
src/web/layout/README.md
Normal 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
7
src/web/ly.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace nulib\web;
|
||||
|
||||
class ly {
|
||||
static function end(): void {
|
||||
}
|
||||
}
|
||||
14
src/web/model/IComponent.php
Normal file
14
src/web/model/IComponent.php
Normal 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
31
src/web/model/IPage.php
Normal 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;
|
||||
}
|
||||
6
src/web/model/IRenderer.php
Normal file
6
src/web/model/IRenderer.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace nulib\web\model;
|
||||
|
||||
interface IRenderer {
|
||||
function render($data): void;
|
||||
}
|
||||
65
src/web/page.php
Normal file
65
src/web/page.php
Normal 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
44
src/web/vo.php
Normal 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
12
tbin/test-vo.php
Normal 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",
|
||||
]);
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<q/></h1><p>hello<nq/><span>brave<q/></span><span>world<nq/></span></p>",
|
||||
c::to_string([
|
||||
[html::H1, "title<q/>"],
|
||||
[html::P, [
|
||||
self::assertSame(<<<EOT
|
||||
<h1>title<q/></h1>
|
||||
<p>hello<nq/><span>brave<q/></span><span>world<nq/></span></p>
|
||||
EOT, c::to_string([
|
||||
[v::H1, "title<q/>"],
|
||||
[v::P, [
|
||||
"hello<nq/>",
|
||||
[html::SPAN, "brave<q/>"],
|
||||
[html::SPAN, ["world<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(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>",
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
82
tests/web/content/TagTest.php
Normal file
82
tests/web/content/TagTest.php
Normal 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
123
tests/web/content/vTest.php
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
])));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user