Skip to content
Snippets Groups Projects
Commit 3178aaef authored by Thomas Flori's avatar Thomas Flori
Browse files

initial commit with view prototype

parents
No related branches found
No related tags found
No related merge requests found
/vendor
composer.lock
{
"name": "tflori/syna",
"description": "Library for rendering native php templates",
"require": {
"php": "^7.1"
},
"autoload": {
"psr-4": {
"Syna\\": "src"
}
},
"require-dev": {
"phpunit/phpunit": "^7.4"
}
}
<?php
use Syna\Engine;
use Syna\HelperLocator;
use Syna\ViewLocator;
foreach ([__DIR__ . '/../../autoload.php', __DIR__ . '/vendor/autoload.php'] as $file) {
if (file_exists($file)) {
require_once $file;
}
}
$viewLocator = new ViewLocator(__DIR__ . '/resources/views');
$layoutLocator = new ViewLocator(__DIR__ . '/resources/layouts');
$helperLocator = new HelperLocator();
$templates = new Engine($viewLocator, $helperLocator, $layoutLocator);
$templates->addSharedData([
'menu' => [
[
'target' => '/home',
'active' => true,
'title' => 'Home',
'icon' => 'home',
'subitems' => [
[
'target' => '/news',
'active' => false,
'title' => 'News',
],
[
'target' => '/blog',
'active' => false,
'title' => 'Blog',
],
],
],
[
'target' => '/products',
'active' => false,
'title' => 'Products',
'icon' => 'pages',
],
],
]);
echo $templates->render('index', [], 'fullPage');
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Example</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Noto+Sans">
<style type="text/css">
nav .nav-wrapper {
max-width: 992px;
margin: 0 auto;
padding: 0 20px;
}
#left-col {
width: 248px;
position: absolute;
z-index: 2;
}
</style>
</head>
<body class="grey lighten-4">
<!-- Toolbar -->
<div class="navbar-fixed">
<nav class="teal darken-1">
<div class="nav-wrapper">
<a href="#" class="sidenav-toggle left hide-on-large-only"><i class="material-icons menu-icon">menu</i><i class="material-icons close-icon">close</i></a>
<a href="/home" class="brand-logo left"><div id="logo-icon"></div><div id="logo-name"></div><div id="logo-subtitle"></div></a>
<ul class="right">
<li class="icon"><a class="btnLogin"><i class="material-icons left">account_circle</i><span class="icon-text"> Login / Signup</span></a></li>
</ul>
<div class="account">
</div>
</div>
</nav>
</div>
<div class="container">
<div class="col l4 hide-on-med-and-down" id="left-col">
<ul class="nav">
<li><a href="/home"><i class="material-icons">home</i> Home</a></li>
<li><a href="/blog"><i class="material-icons">rss_feed</i> Blog</a></li>
<li><a href="/guide"><i class="material-icons">toc</i> Guide</a></li>
<li><a href="/docs"><i class="material-icons">library_books</i> Documentation</a></li>
<li><a href="/exchange"><i class="material-icons">question_answer</i> Exchange</a></li>
</ul>
</div>
<div class="col s12 l9 offset-l3" id="right-col">
<?= $v->section('content') ?>
</div>
</div>
</body>
</html>
<div class="card">
<div class="card-content">
<?= $v->section('content') ?>
</div>
</div>
<?php $v->extend('bigCard'); ?>
<p>Lorem ipsum dolor sit amet....</p>
<?php
namespace Syna;
class Engine
{
protected $sharedData = [];
/** @var HelperLocator */
protected $helperLocator;
/** @var ViewLocator */
protected $viewLocator;
/** @var ViewLocator[] */
protected $namedLocators = [];
/**
* Engine constructor.
* @param ViewLocator $viewLocator
* @param HelperLocator $helperLocator
* @param ViewLocator $layoutLocator
*/
public function __construct(
ViewLocator $viewLocator,
HelperLocator $helperLocator = null,
ViewLocator $layoutLocator = null
) {
$this->viewLocator = $viewLocator;
$this->helperLocator = $helperLocator ?? new HelperLocator();
!$layoutLocator ||
$this->namedLocators['layout'] = $layoutLocator;
}
public function addLocator(string $name, HelperLocator $locator): self
{
$this->namedLocators[$name] = $locator;
return $this;
}
public function addSharedData(array $data): self
{
$this->sharedData = array_merge($this->sharedData, $data);
return $this;
}
public function getSharedData()
{
return $this->sharedData;
}
public function view(string $name, array $data = []): View
{
$viewLocator = $this->viewLocator;
if (strpos($name, '::', 1) !== false) {
list($locatorName, $name) = explode('::', $name);
if (!isset($this->namedLocators[$locatorName])) {
throw new \LogicException('No locator for ' . $locatorName . ' defined');
}
$viewLocator = $this->namedLocators[$locatorName];
}
if (!$viewLocator->has($name)) {
throw new \Exception('View ' . $name . ' not found');
}
return new View($this, $viewLocator->getPath($name), $data);
}
public function render(string $name, array $data = [], string $layout = null): string
{
$view = $this->view($name, $data);
$content = $view->render();
if ($layout && isset($this->namedLocators['layout'])) {
$layout = $this->view('layout::' . $layout);
$layout->setSections(array_merge($view->getSections(), ['content' => $content]));
$content = $layout->render();
}
return $content;
}
public function helper(View $view, $function, ...$arguments)
{
if ($this->helperLocator->has($function)) {
$helper = $this->helperLocator->getHelper($function);
$helper->setView($view);
/** @noinspection PhpMethodParametersCountMismatchInspection */
return $helper(...$arguments);
} elseif (is_callable($function)) {
return call_user_func($function, ...$arguments);
}
throw new \LogicException('$function has to be callable or a registered view helper');
}
}
<?php
namespace Syna;
use Syna\ViewHelper\CallableHelper;
class HelperLocator
{
/** @var array */
protected $map = [];
/** @var array */
protected $namespaces = [ViewHelper::class];
/** @var callable */
protected $resolver;
public function __construct(string $namespace = null, callable $resolver = null)
{
!$namespace ||
$this->namespaces[] = $namespace;
$this->resolver = $resolver ?? function ($class) {
return new $class;
};
}
public function addNamespace(string $namespace): self
{
$this->namespaces[] = $namespace;
return $this;
}
public function prependNamespace(string $namespace): self
{
array_unshift($this->namespaces, $namespace);
return $this;
}
public function add(string $name, $helper)
{
if (isset($this->map[$name])) {
throw new \LogicException('Helper ' . $name . ' already exists');
}
if (is_callable($helper) && !$helper instanceof ViewHelperInterface) {
$this->map[$name] = $helper;
return $this;
}
if ($helper instanceof ViewHelperInterface) {
$this->map[$name] = $helper;
return $this;
}
if (is_string($helper) && class_exists($helper)) {
$this->map[$name] = $helper;
return $this;
}
throw new \LogicException(
'Helper has to be a callable, an instance of ' . ViewHelperInterface::class . ' or a class name'
);
}
public function has(string $name): bool
{
if (isset($this->map[$name])) {
return true;
}
// replace underscores with backslashes and ensure studly case
$baseClassName = implode('\\', array_map('ucfirst', explode('_', $name)));
foreach (array_reverse($this->namespaces) as $namespace) {
$class = $namespace . '\\' . $baseClassName;
if (class_exists($class)) {
$this->map[$name] = $class;
return true;
}
}
return false;
}
public function getHelper(string $name): ViewHelperInterface
{
if (!$this->has($name)) {
throw new \LogicException('View helper ' . $name . ' not found');
}
if (is_callable($this->map[$name]) && !$this->map[$name] instanceof ViewHelperInterface) {
$this->map[$name] = new CallableHelper($this->map[$name]);
} elseif (is_string($this->map[$name])) {
$this->map[$name] = call_user_func($this->resolver, $this->map[$name]);
}
return $this->map[$name];
}
}
<?php
namespace Syna;
class View
{
/** @var Engine */
protected $engine;
/** @var array */
protected $data = [];
/** @var string */
protected $path;
/** @var array */
protected $sections = [];
/** @var View */
protected $parentTemplate;
/** @var string */
protected $sectionName;
/** @var bool */
protected $appendSection = false;
public function __construct(Engine $engine, string $path, array $data = [])
{
$this->engine = $engine;
$this->path = $path;
$this->data = array_merge($engine->getSharedData(), $data);
}
public function __toString()
{
return $this->render();
}
public function __call($name, $arguments)
{
return $this->engine->helper($this, $name, ...$arguments);
}
public function addData(array $data): self
{
$this->data = array_merge($this->data, $data);
return $this;
}
public function setSections(array $sections): self
{
$this->sections = $sections;
return $this;
}
public function getSections(): array
{
return $this->sections;
}
public function render(array $data = null): string
{
if ($data) {
$this->addData($data);
}
unset($data);
$v = $this;
$e = [$this, 'escape'];
extract($this->data, EXTR_SKIP);
$level = ob_get_level();
try {
ob_start();
include $this->path;
$content = trim(ob_get_clean());
if ($this->parentTemplate) {
$this->parentTemplate->setSections(array_merge($this->sections, ['content' => $content]));
$content = $this->parentTemplate->render();
}
return $content;
} catch (\Throwable $exception) {
while (ob_get_level() > $level) {
ob_end_clean();
}
throw $exception;
}
}
public function extend(string $path, array $data = null)
{
$this->parentTemplate = $this->engine->view($path, $data ?? $this->data);
}
public function start(string $name, bool $append = false)
{
if ($name === 'content') {
throw new \LogicException('The section name "content" is reserved.');
}
if ($this->sectionName) {
throw new \LogicException('You cannot nest sections within other sections.');
}
$this->appendSection = $append;
$this->sectionName = $name;
ob_start();
}
public function end()
{
if (is_null($this->sectionName)) {
throw new \LogicException('You must start a section before you can end it.');
}
$content = trim(ob_get_clean());
if ($this->appendSection && isset($this->sections[$this->sectionName])) {
$this->sections[$this->sectionName] .= $content;
} else {
$this->sections[$this->sectionName] = $content;
}
$this->sectionName = null;
$this->appendSection = false;
}
public function section(string $name, string $default = null): string
{
if (!isset($this->sections[$name])) {
return $default;
}
return $this->sections[$name];
}
public function fetch($name, array $data = array())
{
return $this->engine->render($name, $data);
}
public function insert($name, array $data = [])
{
echo $this->engine->render($name, $data);
}
/**
* Apply multiple functions to variable.
* @param mixed $var
* @param string $functions
* @return mixed
*/
public function batch($var, $functions)
{
foreach (explode('|', $functions) as $function) {
$var = $this->engine->helper($this, $function, $var);
}
return $var;
}
/**
* Escape string.
* @param string $string
* @param null|string $functions
* @return string
*/
public function escape($string, $functions = null)
{
static $flags;
if (!isset($flags)) {
$flags = ENT_QUOTES | (defined('ENT_SUBSTITUTE') ? ENT_SUBSTITUTE : 0);
}
if ($functions) {
$string = $this->batch($string, $functions);
}
return htmlspecialchars($string, $flags, 'UTF-8');
}
}
<?php
namespace Syna\ViewHelper;
use Syna\View;
use Syna\ViewHelperInterface;
class CallableHelper implements ViewHelperInterface
{
/** @var callable */
protected $callable;
public function __construct(callable $callable)
{
$this->callable = $callable;
}
public function __invoke(...$args)
{
return call_user_func($this->callable, ...$args);
}
public function setView(View $view)
{
}
}
<?php
namespace Syna;
interface ViewHelperInterface
{
public function __invoke();
public function setView(View $view);
}
<?php
namespace Syna;
class ViewLocator
{
/** @var array */
protected $paths = [];
/** @var array */
protected $namedPaths = [];
/** @var array */
protected $map = [];
/** @var string */
protected $extension = '.phtml';
public function __construct(string $path, string $extension = '.phtml')
{
$this->paths[] = $path;
$this->extension = $extension;
}
public function addPath(string $path): self
{
$this->paths[] = $path;
return $this;
}
public function prependPath(string $path): self
{
array_unshift($this->paths, $path);
return $this;
}
public function add($name, $path): self
{
if (file_exists($path)) {
throw new \LogicException('File ' . $path . ' does not exist');
}
$this->map[$name] = $path;
return $this;
}
public function has($name): bool
{
if (isset($this->map[$name])) {
return true;
}
foreach (array_reverse($this->paths) as $path) {
$viewPath = $path . DIRECTORY_SEPARATOR . $name . $this->extension;
if (file_exists($viewPath)) {
$this->map[$name] = $viewPath;
return true;
}
}
return false;
}
public function getPath($name): string
{
if (!$this->has($name)) {
throw new \Exception('View ' . $name . ' not found');
}
return $this->map[$name];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment