diff --git a/meta/definitions/http.php b/meta/definitions/http.php new file mode 100644 index 0000000..e58261d --- /dev/null +++ b/meta/definitions/http.php @@ -0,0 +1,19 @@ + function ($c) { + return ServerRequestFactory::fromGlobals( + $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES + ); + }, + + EmitterInterface::class => create(SapiEmitter::class), +]; diff --git a/meta/definitions/language.php b/meta/definitions/language.php new file mode 100644 index 0000000..3eee449 --- /dev/null +++ b/meta/definitions/language.php @@ -0,0 +1,31 @@ + create( Tell\I18n::class ) ->constructor( + get(Tell\Reader\JsonReader::class), + get(Tell\Reader\PhpReader::class), + getenv("DEBUG") ? create(Tell\PrintMissingKey::class) : get('tell.fallback') + ), + + 'tell.fallback' => function($c) { + $tell = new Tell\I18n( $c->get(Tell\Reader\PhpReader::class) ); + $tell->locale = "en_US"; + + return $tell; + }, + + # TODO -- accept folders from Lean Apps + Tell\Reader\PhpReader::class => function($c) { + return new Tell\Reader\PhpReader($c->get(Lean\Lean::class)->getI18n('php'), true); + }, + + Tell\Reader\JsonReader::class => function($c) { + return new Tell\Reader\JsonReader($c->get(Lean\Lean::class)->getI18n('json'), true); + }, + + LanguageHandler::class => autowire(LanguageHandler::class), +]; diff --git a/meta/definitions/routes.php b/meta/definitions/routes.php new file mode 100644 index 0000000..46757d6 --- /dev/null +++ b/meta/definitions/routes.php @@ -0,0 +1,86 @@ + autowire(Lean\Routing::class), + + RouteFetcher::class => function($c) { + $fetcher = new RouteFetcher(); + + $fetcher->setFolderList(array_map(function($item) { + return getenv("PROJECT_PATH") . $item; + }, $c->get(Lean\Lean::class)->getRoutable())); + + return $fetcher; + }, + + ApplicationStrategy::class => function($c) { + return new class($c->get(Picea\Picea::class)) extends ApplicationStrategy { + + public Picea\Picea $picea; + + public function __construct(Picea\Picea $picea) { + $this->picea = $picea; + } + + public function getNotFoundDecorator(NotFoundException $exception): MiddlewareInterface + { + return new class($this->picea) implements MiddlewareInterface { + + protected Picea\Picea $picea; + + public function __construct(Picea\Picea $picea) { + $this->picea = $picea; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + return new Zend\Diactoros\Response\HtmlResponse($this->picea->renderHtml("lean/error/404", [], $this), 404); + } + + }; + } + + }; + }, + + 'routes.list' => function($c) { + return function (ContainerInterface $container) { + $router = $container->get(Router::class); + + foreach([ "errorHandler", "dump", SessionMiddleware::class, CronardMiddleware::class, HttpBasicAuthentication::class, JavascriptMiddleware::class ] as $middleware) { + if ( $container->has($middleware) ) { + $router->middleware($container->get($middleware)); + } + } + + $routing = $container->get(Lean\Routing::class); + + $routing->registerRoute($container, getenv('URL_BASE')); + + return $router; + }; + }, +]; diff --git a/meta/definitions/software.php b/meta/definitions/software.php new file mode 100644 index 0000000..6b6984a --- /dev/null +++ b/meta/definitions/software.php @@ -0,0 +1,100 @@ + [ + 'picea' => [ + 'context' => "Lean\\View", + + 'view' => [ + [ + 'path' => getenv("VIEW_PATH"), + 'order' => 10, + ], + [ + 'path' => getenv("PROJECT_PATH") . "/vendor/mcnd/lean/view/", + 'order' => 99, + ], + ], + ], + + 'ulmus' => [ + 'entities' => [ 'Growlogs\\Entity' ] + ], + + 'tell' => [ + 'json' => [ + [ + 'path' => getenv("META_PATH") . "/i18n", + 'order' => 10, + ], + + [ + 'path' => getenv("PROJECT_PATH") . "/vendor/mcnd/lean/meta/i18n", + 'order' => 99, + ], + ], + + 'php' => [ + [ + 'path' => getenv("CACHE_PATH")."/i18n", + 'order' => 0, + ] + ] + ], + + 'routes' => [ + 'Growlogs\\Api' => '/src/Api/', + 'Growlogs\\Dev' => '/src/Dev/', + 'Growlogs\\Web' => '/src/Web/', + ], + ], + + Lean::class => autowire(Lean::class), + + CronardMiddleware::class => function($c) { + $cronardMiddleware = new CronardMiddleware(getenv('CRON_KEY'), function() : ResponseInterface { + return new TextResponse(sprintf("%s - cron task begin...", date('Y-m-d H:i:s'))); + }); + + return $cronardMiddleware->fromFile(getenv("META_PATH")."/crontab.php"); + }, + + JavascriptMiddleware::class => create(JavascriptMiddleware::class), + + Cookie::class => create(Cookie::class)->constructor([ 'secure' => true, 'samesite' => 'Strict' ], getenv("LEAN_RANDOM")), + + Session::class => create(Session::class), + + SessionMiddleware::class => create(SessionMiddleware::class)->constructor(get(Cookie::class), [ 'name' => "lean_sess_" . substr(md5(getenv("LEAN_RANDOM")), 0, 12) ]), + + 'git.commit' => function($c) { + if ( getenv("DEBUG") ) { + return uniqid("debug_"); + } + + $gitdir = getenv("PROJECT_PATH") . DIRECTORY_SEPARATOR . ".git" . DIRECTORY_SEPARATOR; + + if ( false !== ( $currentBranch = file_get_contents( $gitdir . "HEAD") ) ) { + $file = explode(": ", $currentBranch)[1]; + $path = $gitdir . str_replace("/", DIRECTORY_SEPARATOR, trim($file, " \t\n\r")); + + return trim(file_get_contents( $path ), " \t\n\r"); + } + + return "gitless-project"; + }, +]; diff --git a/meta/definitions/template.php b/meta/definitions/template.php new file mode 100644 index 0000000..02aa194 --- /dev/null +++ b/meta/definitions/template.php @@ -0,0 +1,86 @@ + function($c) { + return new Picea(function($html) { + return new HtmlResponse( $html ); + }, $c->get(Context::class), $c->get(Cache::class), $c->get(Compiler::class), null, $c->get(FileFetcher::class), null, getenv("DEBUG")); + }, + + Context::class => function($c) { + return new BaseContext( $c->get(Lean\Lean::class)->getPiceaContext() ); + }, + + Compiler::class => function($c) { + return new Compiler(new class(null, [ + $c->get(LanguageExtension::class), + $c->get(TitleExtension::class), + $c->get(UrlExtension::class), + $c->get(Method\Form::class), + $c->get(Method\Pagination::class), + $c->get(Request::class), + ]) extends DefaultRegistrations { + + public function registerAll(Compiler $compiler) : void + { + parent::registerAll($compiler); + ( new Ui() )->registerFormExtension($compiler); + } + + }); + }, + + Request::class => autowire(Request::class), + + Method\Form::class => autowire(Method\Form::class), + + Method\Pagination::class => autowire(Method\Pagination::class), + + LanguageExtension::class => create(LanguageExtension::class)->constructor(get(Context::class), get(LanguageHandler::class)), + + LanguageHandler::class => function($c) { + return new class( $c->get(Tell\I18n::class) ) implements LanguageHandler { + + public Tell\I18n $tell; + + public function __construct(Tell\I18n $tell) { + $this->tell = $tell; + } + + public function languageFromKey(string $key, array $variables = []) #: array|string + { + return $this->tell->fromKey($key, $variables) ?: ""; + } + }; + }, + + TitleExtension::class => autowire(TitleExtension::class), + + UrlExtension::class => create(UrlExtension::class)->constructor(get(Context::class), getenv("URL_BASE"), get('git.commit')), + + Cache::class => create(Opcache::class)->constructor(getenv("CACHE_PATH"), get(Context::class)), + + FileFetcher::class => function($c) { + return new FileFetcher($c->get(Lean\Lean::class)->getViewPaths()); + }, +]; \ No newline at end of file diff --git a/meta/i18n/en/lean.error.json b/meta/i18n/en/lean.error.json new file mode 100644 index 0000000..dbb3be1 --- /dev/null +++ b/meta/i18n/en/lean.error.json @@ -0,0 +1,9 @@ +{ + "404": { + "title": "Page not found", + "page-title": "404 - Page not found", + "subtitle": "You may have followed an invalid or expired link...", + "message": "It seems like an error occured on the link you visited / action you tried. A notification was sent to the application's developer.", + "back": "Return to previous page" + } +} \ No newline at end of file diff --git a/meta/i18n/fr/lean.error.json b/meta/i18n/fr/lean.error.json new file mode 100644 index 0000000..45d1118 --- /dev/null +++ b/meta/i18n/fr/lean.error.json @@ -0,0 +1,9 @@ +{ + "404": { + "title": "Page introuvable", + "page-title": "404 - Page introuvable", + "subtitle": "Vous avez peut-être suivi un lien expiré ou invalide...", + "message": "Il semblerait que l'action que vous avez tentez à mener vers une page qui n'existe pas / plus. Une notification a été envoyé au développeur de l'application.", + "back": "Revenir à la page précédente" + } +} \ No newline at end of file diff --git a/src/Application.php b/src/Application.php new file mode 100644 index 0000000..e321f84 --- /dev/null +++ b/src/Application.php @@ -0,0 +1,55 @@ +piceaContext = $picea['context']; + } + + if ($picea['view'] ?? false) { + $this->views = $picea['view']; + } + } + + if (is_array($ulmus = $data['ulmus'] ?? false)) { + if ($ulmus['entities'] ?? false) { + $this->entities = $ulmus['entities']; + } + } + + if (is_array($tell = $data['tell'] ?? false)) { + if ($tell['json'] ?? false) { + $this->tellJson = $tell['json']; + } + + if ($tell['php'] ?? false) { + $this->tellPhp = $tell['php']; + } + } + + if (is_array($routes = $data['routes'] ?? false)) { + $this->routes = $data['routes']; + } + + return $this; + } +} \ No newline at end of file diff --git a/src/ControllerTrait.php b/src/ControllerTrait.php index b3914fd..6fcd553 100644 --- a/src/ControllerTrait.php +++ b/src/ControllerTrait.php @@ -29,7 +29,7 @@ use function file_get_contents; */ trait ControllerTrait { - public Session $session; + public Session $session; public ? Picea\Picea $picea; @@ -37,7 +37,7 @@ trait ControllerTrait { public array $contextList = []; - public function __construct(? Picea\Picea $picea, Session $session, ? MailerInterface $mailer = null) { + public function __construct(? Picea\Picea $picea = null, ? Session $session = null, ? MailerInterface $mailer = null) { $this->picea = $picea; $this->session = $session; $this->mailer = $mailer; diff --git a/src/Kernel.php b/src/Kernel.php index 87db266..d78728e 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -112,6 +112,7 @@ class Kernel { protected function serviceContainer() : self { $this->container->has(AdapterProxy::class) and $this->container->get(AdapterProxy::class); + $this->container->has(Lean::class) and $this->container->get(Lean::class); return $this; } diff --git a/src/Lean.php b/src/Lean.php new file mode 100644 index 0000000..d3d91b6 --- /dev/null +++ b/src/Lean.php @@ -0,0 +1,99 @@ +container = $container; + + $this->loadApplications(); + } + + protected function loadApplications() : void + { + $list = $this->container->get('config')['lean']['autoload'] ?? []; + + if (! $list ) { + throw new \Exception("You must provide at least one application to autoload within your config file ( 'lean' => 'autoload' => [] )"); + } + + foreach($list as $application) { + if ( $this->container->has($application) ) { + $this->applications[] = ( new Application() )->fromArray($this->container->get($application)); + } + else { + throw new \RuntimeException("Trying to load an application '$application' which have not been configured yet"); + } + } + } + + public function getPiceaContext() : string + { + foreach(array_reverse($this->applications) as $apps) { + if ( $apps->piceaContext ) { + return $apps->piceaContext; + } + } + + return static::DEFAULT_PICEA_CONTEXT; + } + + public function getRoutable() : array + { + return array_merge(...array_map(fn($app) => $app->routes ?? [], $this->applications)); + } + + public function getViewPaths() : array + { + $list = array_merge(...array_map(fn($app) => $app->views ?? [], $this->applications)); + + uasort($list, fn($i1, $i2) => $i1['order'] <=> $i2['order'] ); + + return $list; + } + + public function getI18n(string $reader) : ? array + { + switch($reader) { + case "php": + $list = array_merge(...array_map(fn($app) => $app->tellPhp ?? [], $this->applications)); + break; + + case "json": + $list = array_merge(...array_map(fn($app) => $app->tellJson ?? [], $this->applications)); + break; + } + + if ( $list ?? false ) { + uasort($list, fn($i1, $i2) => $i2['order'] <=> $i1['order']); + + return array_map(fn($item) => $item['path'], $list); + } + + return null; + } + + public static function definitions() : array + { + $path = dirname(__DIR__) . "/meta/definitions/"; + + return array_merge( + require($path . "http.php"), + require($path . "language.php"), + require($path . "routes.php"), + require($path . "software.php"), + require($path . "template.php"), + ); + } +} \ No newline at end of file diff --git a/view/lean/error/404.phtml b/view/lean/error/404.phtml new file mode 100644 index 0000000..e2984e0 --- /dev/null +++ b/view/lean/error/404.phtml @@ -0,0 +1,24 @@ +{% extends "lean/layout/error" %} + +{% language.set "lean.error.404" %} + +{% title _('page-title') %} + +{% section "content-right" %} +
+{% endsection %} + +{% section "content-left" %} + +{% endsection %} + +{% section "head.css" %} + .title {font-size:2rem} + .subtitle {font-size:1.25rem; padding-top: 1rem;} + .content {padding-top:1rem} +{% endsection %} diff --git a/view/lean/layout/error.phtml b/view/lean/layout/error.phtml new file mode 100644 index 0000000..c3f8de6 --- /dev/null +++ b/view/lean/layout/error.phtml @@ -0,0 +1,50 @@ +{% section "layout" %} + + + + + +